├── License.txt ├── Readme.md ├── composer.json ├── config ├── container.php └── router.yaml ├── contrib.md ├── docker-compose.yml ├── docs ├── cluster.md ├── dispatch.md ├── fe_handlers.md ├── fetch.md ├── index.md ├── listen.md ├── message_requests.md ├── message_responses.md ├── redis.md ├── router.md ├── router_auto.md ├── router_params.md ├── router_php.md └── router_yaml.md ├── examples ├── App │ ├── Shop │ │ ├── OrderModel.php │ │ ├── Orders.php │ │ └── Users.php │ ├── Users │ │ ├── Login.php │ │ ├── Profiles.php │ │ └── UserModel.php │ └── Wallets │ │ └── Bitcoin.php ├── Readme.md ├── autorouting.php ├── basic.php ├── init.php ├── instances.php ├── listen.php ├── multi.php ├── params.php ├── queue.php └── yaml │ ├── autorouting.yml │ ├── basic.yml │ ├── full.yml │ ├── instances.yml │ ├── multi.yml │ ├── params.yml │ └── queue.yml ├── phpunit.xml ├── signatures.json ├── src ├── Brokers │ ├── Local.php │ └── RabbitMQ.php ├── Cluster.php ├── Dispatcher.php ├── Exceptions │ ├── ClusterDuplicateRoutingAliasException.php │ ├── ClusterExchangeTypeOverlayException.php │ ├── ClusterFileNotWriteableException.php │ ├── ClusterInvalidArgumentException.php │ ├── ClusterInvalidParamOperatorException.php │ ├── ClusterInvalidRoutingKeyException.php │ ├── ClusterOutOfBountsException.php │ ├── ClusterParamValueNotExistsException.php │ ├── ClusterPhpClassNotExistsException.php │ ├── ClusterTimeoutException.php │ ├── ClusterYamlConfigException.php │ └── ClusterZeroRoutesException.php ├── FeHandlers │ ├── Generic.php │ └── Syrus.php ├── Fetcher.php ├── Interfaces │ ├── BrokerInterface.php │ ├── FeHandlerInterface.php │ ├── MessageRequestInterface.php │ ├── MessageResponseInterface.php │ └── ReceiverInterface.php ├── Listener.php ├── Message │ ├── MessageRequest.php │ └── MessageResponse.php ├── Receiver.php ├── Router │ ├── InstanceMap.php │ ├── Loaders │ │ ├── RedisLoader.php │ │ └── YamlLoader.php │ ├── Mapper.php │ ├── ParamChecker.php │ ├── Router.php │ └── Validator.php └── Sys │ └── Sys.php └── tests ├── router_test.php └── yaml_loader_test.php /License.txt: -------------------------------------------------------------------------------- 1 | 2 | Permission is hereby granted, free of charge, to any person 3 | obtaining a copy of this software and associated documentation 4 | files (the "Software"), to deal in the Software without 5 | restriction, including without limitation the rights to use, copy, 6 | modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be 11 | included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 18 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ======================================================================== 22 | (END LICENSE TEXT) 23 | 24 | The MIT license is compatible with both the GPL and commercial 25 | software, affording one all of the rights of Public Domain with the 26 | minor nuisance of being required to keep the above copyright notice 27 | and license text in the source code. Note also that by accepting the 28 | Public Domain "license" you can re-license your copy using whatever 29 | license you like. 30 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Cluster - Load Balancer / Router for Horizontal Scaling 3 | 4 | Cluster provides a simple yet intuitive interface to implement horizontal scaling across your application via RabbitMQ or any message broker. With the local message broker, easily develop your software with horizontal scaling fully implemented while running on one server, and split into multiple server instances within minutes when necessary. It supports: 5 | 6 | * General round robin and routing of designated messages to specific server instances. 7 | * One-way queued messages, two-way RPC calls, and system wide broadcasts. 8 | * Parameter / header based routing. 9 | * Easy to configure YAML router file. 10 | * Optional centralized redis storage of router configuration for maintainability across server instances. 11 | * Standardized immutable request and response objects for ease-of-use and interopability. 12 | * Front-end handlers for streamlined communication back to front-end servers allowing execution of events to the client side (eg. set template variables, et al). 13 | * Timeout and message preparation handlers, plus concurrency settings. 14 | * Interchangeable with any other message broker including the ability to easily implement your own. 15 | * Includes local message broker, allowing implementation of logic for horizontal scaling while remaining on one server instance. 16 | * Optional auto-routing allowing messages to be automatically routed to correct class and method that correlates to named routing key. 17 | 18 | ## Table of Contents 19 | 20 | 1. [Cluster class / Container Definitions](https://github.com/apexpl/cluster/blob/master/docs/cluster.md) 21 | 2. [Router Overview](https://github.com/apexpl/cluster/blob/master/docs/router.md) 22 | 1. [Adding Routes in PHP](https://github.com/apexpl/cluster/blob/master/docs/router_php.md) 23 | 2. [Router YAML Configuration File](https://github.com/apexpl/cluster/blob/master/docs/router_yaml.md) 24 | 3. [Auto Routing](https://github.com/apexpl/cluster/blob/master/docs/router_auto.md) 25 | 4. [Parameter Based Routing](https://github.com/apexpl/cluster/blob/master/docs/router_params.md) 26 | 5. [Enable redis Autoloading](https://github.com/apexpl/cluster/blob/master/docs/redis.md) 27 | 3. Message Handling 28 | 1. [Listen / Consume Messages](https://github.com/apexpl/cluster/blob/master/docs/listen.md) 29 | 2. [Dispatch Messages](https://github.com/apexpl/cluster/blob/master/docs/dispatch.md) 30 | 3. [Fetch Messages from Queues](https://github.com/apexpl/cluster/blob/master/docs/fetch.md) 31 | 4. Messages 32 | 1. [Message Requests](https://github.com/apexpl/cluster/blob/master/docs/message_requests.md) 33 | 2. [Message Responses](https://github.com/apexpl/cluster/blob/master/docs/message_responses.md) 34 | 5. [Front-End Handlers](https://github.com/apexpl/cluster/blob/master/docs/fe_handlers.md) 35 | 36 | ## Installation 37 | 38 | Install via Composer with: 39 | > `composer require apex/cluster` 40 | 41 | 42 | ## Basic Usage 43 | 44 | Please see the /examples/ directory for more in-depth examples. 45 | 46 | **Save Math.php Class** 47 | ~~~php 48 | 49 | namespace App; 50 | 51 | class Math { 52 | 53 | public function add(MessageRequestInterface $msg) 54 | { 55 | list($x, $y) = $msg->getParams(); 56 | return ($x + $y); 57 | } 58 | } 59 | ~~~ 60 | 61 | 62 | **Define Listener** 63 | ~~~php 64 | use Apex\Cluster\Cluster; 65 | use Apex\Cluster\Listener; 66 | use Apex\Cluster\Brokers\RabbitMQ; 67 | 68 | 69 | // Start cluster 70 | $cluster = new Cluster('app1'); 71 | $cluster->setBroker(new RabbitMQ('localhost', 5672, 'guest', 'guest')); 72 | $cluster->addRoute('basic.math.*', App\Math::class); 73 | 74 | // Start listener 75 | $listener = new Listener(); 76 | $listener->listen(); 77 | ~~~ 78 | 79 | **Define Dispatcher** 80 | ~~~php 81 | use Apex\Cluster\Dispatcher; 82 | use Apex\Cluster\Message\MessageRequest; 83 | 84 | // Define message 85 | $msg = new MessageRequest('basic.math.add', 6, 9); 86 | 87 | // Dispatch message 88 | $dispatcher = new Dispatcher('web1'); 89 | $sum = $dispatcher->dispatch($msg)->getResponse(); 90 | 91 | // Print result 92 | echo "Sum is: $sum\n"; 93 | ~~~ 94 | 95 | 96 | 97 | ## Follow Apex 98 | 99 | Loads of good things coming shortly including new quality open source packages, more advanced articles / tutorials that go over down to earth useful topics, et al. Stay informed by joining the mailing list on our web site, or follow along on Twitter at @mdizak1. 100 | 101 | 102 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apex/cluster", 3 | "description": "Load Balancer / Router for Horizontal Scaling", 4 | "type": "package", 5 | "homepage": "https://apexpl.io", 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=8.0", 9 | "php-amqplib/php-amqplib": "^3.0", 10 | "symfony/yaml": "^6.0", 11 | "apex/container": ">=2.0", 12 | "symfony/string": "^6.0", 13 | "psr/event-dispatcher": "^1.0", 14 | "monolog/monolog": "^3.4" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^9.5", 18 | "apex/signer": "^2.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Apex\\Cluster\\": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | Local::class, 24 | BrokerInterface::class => [RabbitMQ::class, ['host' => 'localhost', 'port' => 5672, 'user' => 'guest', 'password' => 'guest']], 25 | 26 | /** 27 | * Front-end handler. Generally the template engine you're using if supported, and allows front-end actions such as 28 | * assigning template variables to be passed back to and processed by dispatcher. See /docs/fe_handlers.md file for details. 29 | */ 30 | FeHandlerInterface::class => Apex\Cluster\FeHandlers\Syrus::class, 31 | 32 | /** 33 | * PSR3 compliant logger, defaults to the popular Monolog package. Set to null to disable logging. 34 | */ 35 | LoggerInterface::class => function() { return new Logger('cluster', [new StreamHandler(__DIR__ . '/../cluster.log')]); }, 36 | 37 | /** 38 | * Timeout handler. If RPC call times out, this is called. 39 | */ 40 | 'cluster.timeout_seconds' => 3, 41 | //'cluster.timeout_handler' => function (MessageRequestInterface $msg) { echo "We've timed out with key: " . $msg->getRoutingKey() . "\n"; exit; }, 42 | 43 | /** 44 | * Message preparation handler. If defined, this closure will be invoked for every incoming message and is meant to 45 | * prepare your specific envrionment for processing of messages. 46 | */ 47 | //'cluster.prepare_msg_handler' => function (MessageRequestInterface $msg) { }, 48 | 49 | /** 50 | * Front-end Handler Callback. If defined, will be invoked for every message dispatched upon receiving a response with 51 | * a FeHandlerInterface object passed. Used to update output to end-users. See docs for details. 52 | */ 53 | //'cluster.fe_handler_callback' => function (FeHandlerInterface $handler) { }, 54 | 55 | /** 56 | * Custom router. This should almost always be left commented out, but allows you to utilize a custom router. Should returnan 57 | * associative array, keys being the response alias (eg. default) and values being the full namespace / class name that should be called for the given routing key. 58 | */ 59 | //'cluster.custom_router' => function(MessageRequestInterface $msg) { }, 60 | 61 | /** 62 | * Additional items, you probably don't need to modify these. 63 | */ 64 | ReceiverInterface::class => Receiver::class 65 | ]; 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /config/router.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Remove this line if you want Cluster to utilize the configuration below. 3 | ignore: true 4 | 5 | #################### 6 | # Routes 7 | # 8 | # Below defines all routes that are configured across all server 9 | # instances. This informs Cluster which PHP classes to route which messages to. 10 | # 11 | # Please see the /docs/router_yaml.md file for details. 12 | #################### 13 | 14 | routes: 15 | 16 | rpc.default: 17 | type: rpc 18 | instances: all 19 | routing_keys: 20 | all: App\~package.title~\~module.title~ 21 | 22 | queue.default: 23 | type: queue 24 | instances: all 25 | routing_keys: 26 | all: App\~package.title~\~module.title~ 27 | 28 | broadcast.cluster: 29 | type: broadcast 30 | instances: all 31 | routing_keys: 32 | cluster.sys: Apex\Cluster\Sys\Sys 33 | 34 | 35 | -------------------------------------------------------------------------------- /contrib.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | Contributions always welcome, and proper credit will always be given to any contributors. Standard rules apply: 5 | 6 | - PSR compliant code. 7 | - Must include any necessary tests. 8 | - One contribution per PR 9 | - Please prefix PRs as necessary (eg. hotfix/some_fix for fixes, feature/cool_feature for new feature, etc.). 10 | 11 | ## Moving Forward 12 | 13 | Things that come to mind that could be developed into Cluster: 14 | 15 | * Additional front-end endlers for platforms such as Wordpress, Twig, Smarty, et al. 16 | * Attributes support. Auto generate YAML configuration file by scanning directory of PHP classes, look for a "listensTo()" attribute or similar, and generates routes based on that. 17 | * Look into asynchronous calls, which I believe is somewhat already there via the Listener::dispatch() method, which declares all exchanges and queues without calling $broker->wait(). 18 | * Testing of AWS SQS, and implementation of another BrokerInterface if necessary. 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: '3' 3 | 4 | services: 5 | 6 | rabbitmq: 7 | image: rabbitmq:3.8 8 | restart: unless-stopped 9 | ports: 10 | - "5672:5672" 11 | working_dir: /var/www 12 | networks: 13 | - cluster 14 | 15 | 16 | networks: 17 | cluster: 18 | driver: bridge 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/cluster.md: -------------------------------------------------------------------------------- 1 | 2 | # Cluster class / Container Definitions 3 | 4 | The central class within Cluster, and facilitates all functionality. Although required for all functionality, the `Dispatcher`, `Listener` and `Fetcher` classes will automatically instantiate the `Cluster` class if has not already been done. However, if you're utilizing either redis or non-default locations for either the container definitions or YAML router files, you will need to instantiate the `Cluster` class each time. 5 | 6 | The constructor accepts four arguments: 7 | 8 | Variable | Required | Type | Description 9 | ------------- |------------- |------------- |------------- 10 | `$instance_name` | Yes | string | The unique name of the server instance (eg. app1, app2, web1, web2, etc.) 11 | `$redis` | No | redis | If a redis connection is passed, configuration will be auto-loaded from within redis while ignoring local container definition and YAML router files. 12 | `$router_file` | No | string | Location of the YAML router file to load. Defaults to ~/config/router.yml 13 | `$container_file` | No | string | Container definitions file to load. Defaults to ~/config/container.php 14 | 15 | For example: 16 | 17 | ~~~php 18 | use Apex\Cluster\Cluster; 19 | use Apex\Cluster\Listener; 20 | 21 | // Start cluster 22 | $cluster = new Cluster( 23 | instance_name: 'app1', 24 | router_file: /path/to/my/router.yml, 25 | container_file: /path/to/container.php 26 | ); 27 | 28 | // Start listening 29 | $listener = new Listener(); 30 | $listener->listen(); 31 | ~~~ 32 | 33 | 34 | ## Container Definitions File 35 | 36 | The default location of the container definitions file is at ~/config/container.php, although a different location can be defined via the `$container_file` variable as the fourth argument when instantiating the Cluster class. 37 | 38 | The below table describes all items found within this file, and modify accordingly to your specific needs. Most importantly, you may want to modify the message broker being used as it defaults to local, and the timeout handler which is called when RPC calls timeout. 39 | 40 | Item | Description 41 | ------------- |------------- 42 | `BrokerInterface::class` | The message broker being used, defaults to the local broker but can easily be switched to RabbitMQ as shown in the default container file. 43 | `FeHandlerInterface::class` | The front-end handler being used, which helps allow back-end consumer instances to effect changes on the front-end output (eg. assign template variables, change http status, et al). Please view the Front-end Endhandlers page of this documentation for details. 44 | `LoggerInterface::class` | Any PSR-3 compliant logger, defaults to the popular Monolog package. If left as is, logs will appear within the ~/cluster.log file. To disable logging, simply comment out this line. 45 | `timeout_seconds` | The number of seconds without a response before a RPC call times out. 46 | timeout_handler | Must be a callable / closure, and accepts one argument being a `MessageRequestInterface` of the message being sent. If defined, will be called when an RPC call times out. 47 | prepare_msg_handler | If this closure is defined, every incoming message received will be passed through it, and is meant to prepare your environment as necessary to process the message, such as injecting inputs into a framework, instantiating an auth session, et al. Note, a callback may also be passed to the `Listener::listen()` method which overrides this setting. 48 | fe_handler_callback | If this closure is defined, it will be invoked every time a dispatcher receives a response to a RPC call, and is meant to process all actions within the front-end handler to change output to the end-user. Note, a callback can be passed to the `Dispatcher::dispatch()` method which overrides this setting. Please see the [Front-End Handlers](fe_handlers.md) page for details. 49 | `custom_router` | Generally not required, but must be a callable / closure and only needed if you wish to override the built-in router altogether. Takes one argument being an instance of `MessageRequestInterface`, and must return an associative array, keys being the response alias (eg. "default") and the values being full class name to the PHP class to execute. 50 | `ReceiverInterface::class` | Should almost always be left as is, and only modified if you plan to write your own version of the /src/Receiver.php file. 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/dispatch.md: -------------------------------------------------------------------------------- 1 | 2 | # Dispatch Messages 3 | 4 | Dispatching messages and receiving responses is very simplistic with the `Dispatcher` class, and as easy as: 5 | 6 | ~~~php 7 | use Apex\Cluster\Dispatcher; 8 | use Apex\Cluster\Message\MessageRequest; 9 | 10 | // Define message 11 | $msg = new MessageRequest('users.profile.load', $var1, $var2, $obj1, $obj2 12 | 13 | // Send message, and get response 14 | $dispatcher = new Dispatcher('web1'); 15 | $user = $dispatcher->dispatch($msg)->getResponse(); 16 | ~~~ 17 | 18 | That's all there really is to it. Few notes regarding the above: 19 | 20 | * The `MessageRequest` object can take as many parameters as desired, and may be anything you wish as long as it can be serialized. Please see the [Message Requests](message_requests.md) page for details. 21 | * For RPC calls, will always return a `MessageResponseInterface` object. Please see the [Message Responses](message_responses.md) page for details. 22 | * If sending a "queue" or "broadcast" message type, will return null as both are one-way only messages. 23 | * The first argument when instantiating the `Dispatcher` class is the optional unique name of the server instance, allowing listening consumers to know which server instance the message originated from. 24 | 25 | 26 | The `Dispatcher::dispatch()` method takes three arguments: 27 | 28 | Variable: | Required | Type | Description 29 | ------------- |------------- |------------- |------------- 30 | `$msg` | Yes | MessageRequestInterface | The message being dispatched. Please see the [Message Requests](message_requests.md) page for details. 31 | `$msg_type` | No | string | The message type to dispatch, supported types are -- rpc, queue, broadcast. Defaults to "rpc". 32 | `$fe_handler_callback` | No | callable | If defined, will override any "fe_handler_callback" closure defind within the container definitions file, and will be invoked with the `FeHandlerInterface` object passed upon receiving a response from the consumer. Used to update the output to end-user, and please see [Front-End Handlers](fe_handlers.md) page for details. 33 | 34 | 35 | ## Dispatch queue / broadcast Messages 36 | 37 | Three different types of messages can be dispatched -- rpc, queue, broadcast. The default is "rpc", but you may send a "queue" or "broadcast" message type by specifying it as the second argument of the `Dispatcher::dispatch()` method, such as: 38 | 39 | ~~~php 40 | use Apex\Cluster\Dispatcher; 41 | use Apex\Cluster\Message\MessageRequest; 42 | 43 | // Define message 44 | $msg = new MessageRequest('gallery.images.upload', $image_filename); 45 | 46 | // Dispatch 47 | $dispatcher = new Dispatcher('web1'); 48 | $dispatcher->dispatch($msg, 'queue'); 49 | ~~~ 50 | 51 | Please note, "queue" and "broadcast" messages do not return a response, and only return null. 52 | 53 | 54 | ## Front-End Handler Callback 55 | 56 | Within the container definitions file (eg. ~/config/container.php) you may define a "fe_handler_callback" closure, which if defined, will be invoked for every message dispatched with a `FeHandlerInterface` object passed to it. Alternatively, you may pass a callback as the third argument to the `Dispatcher::dispatch()` method, which will override any closure specified within the container definitions file. 57 | 58 | This helps streamline the process of the back-end consumer instances affecting change on the output to the front-end (eg. assign template variables, add callouts, etc.). For full information, please visit the [Front-End Handlers](fe_handlers.md) page of this documentation. 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/fe_handlers.md: -------------------------------------------------------------------------------- 1 | 2 | # Front-End Handlers 3 | 4 | Cluster provides support for front-end handlers, helping streamline the process of back-end consumer instances affecting output of the front-end instances (eg. assign template variables, change HTTP status code, et al). 5 | 6 | The front-end handler being utilized can be set within the container definitions file (ie. ~/config/container.php) as the `FeHandlerInterface::class` item. All supported front-end handlers can be found within the ~/src/FeHandlers directory, the default being Generic. 7 | 8 | ## Usage 9 | 10 | In short, the process flow for front-end handlers is: 11 | 12 | 1. When a consumer receives a message, it will instantiate the `FeHandlerInterface`, and pass it as the second argument to all consumer methods being called. 13 | 2. The `FeHandlerInterface` object acts as a queue for all actions that need to be performed on the front-end. The methods available depend on the handler class being utilized, but the generic `addAction($action, $data)` method can also be used to add actions to the queue (see below). 14 | 3. Upon the dispatcher receiving a response it will invoke the necessary callback and pass the `FeHandlerInterface` object to it. The callback invoked will either be the callable passed as the third argument to the `Dispatcher::dispatch()` method, or the "fe_hander_callback" closure defined within the container definitions file. 15 | 4. The closure should retrieve all actions from the `FeHandlerInterface` class and perform the necessary actions on the front-end. 16 | 17 | Simple as that, and below is a brief example: 18 | 19 | **Dispatcher** 20 | ~~~php 21 | use Apex\Cluster\Dispatcher; 22 | use Apex\Cluster\Message\MessageRequest; 23 | use Apex\Cluster\Interfaces\FeHandlerInterface; 24 | 25 | // Define callback 26 | $callback = function(FeHandlerInterface $handler) { 27 | 28 | $actions = $handler->getActions(); 29 | foreach ($actions as $vars) { 30 | $action = $vars[0]; 31 | $data = $vars[1]; 32 | 33 | if ($action == 'set_var') { 34 | echo "Assigning Template Variable: $data[0] = $data[1]
\n"; 35 | } 36 | }; 37 | 38 | // Set message 39 | $msg = new MessageRequest('transaction.orders.add', 58.35, 'XYZ Product'); 40 | 41 | // Dispatch a message 42 | $dispatcher = new Dispatcher('web1'); 43 | $response = $dispatcher->dispatch($msg, 'rpc', $callback); 44 | 45 | // Get response 46 | $order_id = $response->getResponse(); 47 | echo "Order ID: $order_id\n"; 48 | ~~~ 49 | 50 | 51 | **Consumer** 52 | 53 | This assumes you already have the appropriate [routing](router.md) in place so messages sent to the routing key "transaction.orders.*" are routed to this PHP class. 54 | 55 | ~~~php 56 | 57 | use Apex\Cluster\Interfaces\{MessageRequestInterface, FeHandlerInterface}; 58 | 59 | class orders 60 | { 61 | 62 | public function add(MessageRequestInterface $msg, FeHandlerInterface $fe_handler) 63 | { 64 | 65 | // Add new order, and generate order id 66 | $order_id = 12345; 67 | 68 | // Assign template variable to front-end 69 | $fe_handler->addAction('set_var', ['order_id', $order_id]); 70 | 71 | // Return 72 | return $order_id; 73 | } 74 | 75 | } 76 | ~~~ 77 | 78 | 79 | Running the above dispatcher would result in: 80 | 81 | ~~~ 82 | Assigning Template Variable: order_id = 12345
83 | Order ID: 12345 84 | ~~~ 85 | 86 | The consumer method added an action to the `FeHandlerInterface` to assign a template variable. Upon the dispatcher receiving the response, it invoked the callback passed, which should go through all actions within the `FeHandlerInterface` object and assign any necessary template variables, or complete any necessary actions that affect the output to the end-user. 87 | 88 | 89 | -------------------------------------------------------------------------------- /docs/fetch.md: -------------------------------------------------------------------------------- 1 | 2 | # Fetch Messages from Queues 3 | 4 | Using the `Fetcher` class you can fetch messages from a queue that have been previously dispatched and are awaiting processing. Please note, if you plan on using this functionality such as allowing messages to build up in a queue, then process via a crontab job or similar, you should use a separate YAML router file to define the queues. This will ensure the messages don't get passed on to an active listener. 5 | 6 | Instead of instantiating the `Cluster` class as you normally would, write a separate YAML router file only for the queues that will be fetched from, and instantiate `Cluster` with that file: 7 | 8 | ~~~php 9 | use Apex\Cluster\Cluster; 10 | 11 | $cluster = new Cluster( 12 | instance_name: fetch1, 13 | router_file: /path/to/queues.yml 14 | ); 15 | ~~~ 16 | 17 | 18 | ## Fetching From Queues 19 | 20 | Fetching pending messages from queus is very simplistic, and for example: 21 | 22 | ~~~php 23 | use Apex\Cluster\Fetcher; 24 | 25 | $fetcher = new Fetcher('fetch1'); 26 | while ($msg = $fetcher->fetch('some_queue_name)) { 27 | 28 | $params = $msg->getParams(); 29 | 30 | // Process message... 31 | } 32 | ~~~ 33 | 34 | That's all there is to it. The one argument passed to the `Fetcher::fetch()` method needs to be the queue name which the route definition was created as. Within the YAML router file, every route definition has a unique name, and this is the queue name you pass to fetch messages. 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | # Cluster - Load Balancer / Router for Horizontal Scaling 3 | 4 | Cluster provides a simple yet intuitive interface to implement horizontal scaling across your application via RabbitMQ or any message broker. With the local message broker, easily develop your software with horizontal scaling fully implemented while running on one server, and split into multiple server instances within minutes when necessary. It supports: 5 | 6 | * General round robin and routing of designated messages to specific server instances. 7 | * One-way queued messages, two-way RPC calls, and system wide broadcasts. 8 | * Parameter / header based routing. 9 | * Easy to configure YAML router file. 10 | * Optional centralized redis storage of router configuration for maintainability across server instances. 11 | * Standardized immutable request and response objects for ease-of-use and interopability. 12 | * Front-end handlers for streamlined communication back to front-end servers allowing execution of events to the client side (eg. set template variables, et al). 13 | * Timeout and message preparation handlers, plus concurrency settings. 14 | * Interchangeable with any other message broker including the ability to easily implement your own. 15 | * Includes local message broker, allowing implementation of logic for horizontal scaling while remaining on one server instance. 16 | * Optional auto-routing allowing messages to be automatically routed to correct class and method that correlates to named routing key. 17 | 18 | ## Table of Contents 19 | 20 | 1. [Cluster class / Container Definitions](https://github.com/apexpl/cluster/blob/master/docs/cluster.md) 21 | 2. [Router Overview](https://github.com/apexpl/cluster/blob/master/docs/router.md) 22 | 1. [Adding Routes in PHP](https://github.com/apexpl/cluster/blob/master/docs/router_php.md) 23 | 2. [Router YAML Configuration File](https://github.com/apexpl/cluster/blob/master/docs/router_yaml.md) 24 | 3. [Auto Routing](https://github.com/apexpl/cluster/blob/master/docs/router_auto.md) 25 | 4. [Parameter Based Routing](https://github.com/apexpl/cluster/blob/master/docs/router_params.md) 26 | 5. [Enable redis Autoloading](https://github.com/apexpl/cluster/blob/master/docs/redis.md) 27 | 3. Message Handling 28 | 1. [Listen / Consume Messages](https://github.com/apexpl/cluster/blob/master/docs/listen.md) 29 | 2. [Dispatch Messages](https://github.com/apexpl/cluster/blob/master/docs/dispatch.md) 30 | 3. [Fetch Messages from Queues](https://github.com/apexpl/cluster/blob/master/docs/fetch.md) 31 | 4. Messages 32 | 1. [Message Requests](https://github.com/apexpl/cluster/blob/master/docs/message_requests.md) 33 | 2. [Message Responses](https://github.com/apexpl/cluster/blob/master/docs/message_responses.md) 34 | 5. [Front-End Handlers](https://github.com/apexpl/cluster/blob/master/docs/fe_handlers.md) 35 | 36 | ## Installation 37 | 38 | Install via Composer with: 39 | > `composer require apex/cluster` 40 | 41 | 42 | ## Basic Usage 43 | 44 | Please see the /examples/ directory for more in-depth examples. 45 | 46 | **Save Math.php Class** 47 | ~~~php 48 | 49 | namespace App; 50 | 51 | class Math { 52 | 53 | public function add(MessageRequestInterface $msg) 54 | { 55 | list($x, $y) = $msg->getParams(); 56 | return ($x + $y); 57 | } 58 | } 59 | ~~~ 60 | 61 | 62 | **Define Listener** 63 | ~~~php 64 | use Apex\Cluster\Cluster; 65 | use Apex\Cluster\Listener; 66 | use Apex\Cluster\Brokers\RabbitMQ; 67 | 68 | 69 | // Start cluster 70 | $cluster = new Cluster('app1'); 71 | $cluster->setBroker(new RabbitMQ('localhost', 5672, 'guest', 'guest')); 72 | $cluster->addRoute('basic.math.*', App\Math::class); 73 | 74 | // Start listener 75 | $listener = new Listener(); 76 | $listener->listen(); 77 | ~~~ 78 | 79 | **Define Dispatcher** 80 | ~~~php 81 | use Apex\Cluster\Dispatcher; 82 | use Apex\Cluster\Message\MessageRequest; 83 | 84 | // Define message 85 | $msg = new MessageRequest('basic.math.add', 6, 9); 86 | 87 | // Dispatch message 88 | $dispatcher = new Dispatcher('web1'); 89 | $sum = $dispatcher->dispatch($msg)->getResponse(); 90 | 91 | // Print result 92 | echo "Sum is: $sum\n"; 93 | ~~~ 94 | 95 | 96 | 97 | ## Follow Apex 98 | 99 | Loads of good things coming shortly including new quality open source packages, more advanced articles / tutorials that go over down to earth useful topics, et al. Stay informed by joining the mailing list on our web site, or follow along on Twitter at @mdizak1. 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/listen.md: -------------------------------------------------------------------------------- 1 | 2 | # Listen / Consume Messages 3 | 4 | Starting a new listener to consume messages is easily done via the `Listener` class, and can be as simple as: 5 | 6 | ~~~php 7 | use Apex\Cluster\Listener; 8 | 9 | // Start listening 10 | $listener = new Listener('app3'); 11 | $listener->listen(); 12 | ~~~ 13 | 14 | The `Listener` class accepts one argument being the unique name for this instance, which may affect the messages routed to it depending on your router configuration. The above works fine assuming both, you are using YAML based router configuration, and are using the default ~/config/router.yml location for your router file. Otherwise, you will need to instantiate the `Cluster` class first, such as: 15 | 16 | ~~~php 17 | use Apex\Cluster\Cluster; 18 | use Apex\Cluster\Listener; 19 | 20 | // Start cluster 21 | $cluster = new Cluster( 22 | instance_name: 'app1', 23 | router_file: /path/to/router.yml 24 | ); 25 | 26 | // Add necessary routes 27 | $cluster->addRoute(....); 28 | 29 | // Start listening 30 | $listener = new Listener(); 31 | $listener->listen(); 32 | ~~~ 33 | 34 | The `Listener::listen()` method accepts three optional arguments, described in the below table: 35 | 36 | Variable | Type | Description 37 | ------------- |------------- |------------- 38 | `$screen_logging` | bool | Whether or not to print logs to the screen. Defaults to true. 39 | `$max_msg` | int | Only applies to routes of message type "queue", and is the number of concurrent messages the listener will accept. Defaults to YAML configuration if defined, or to 1. 40 | `$prepare_msg_handler` | callable | If defined, will be invoked for every message received to prepare environment for processing. This will override any "prepare_msg_handler" closure set within the container definitions file. Defaults to null. 41 | 42 | 43 | ## PHP Consumers 44 | 45 | A few notes regarding PHP consumers: 46 | 47 | * The PHP classes that act as consumers may be located anywhere, and please review documentation regarding the router for details on how to specify which PHP classes get invoked for incoming messages. 48 | * All routing keys comprise of three segments seperated by periods. The third segment is always the name of the method that will be called within the PHP consumers. 49 | * Two arguments are passed to every method, a `MessageRequestInterface` and `FeHandlerInterface`. Please see the [Message Requests](message_requests.md) and [Front-End Handlers](fe_handers.md) pages for full details on both arguments. 50 | * The methods may return anything you wish that can be serialized, and for PRC calls it will be returned to the dispatcher. This includes variables, arrays and objects, but does not include closures or resources. 51 | 52 | For example, if the router is configured so all messages to the routing key "users.profile.*" are routed to the PHP class `App\Users\Profiles`: 53 | 54 | ~~~php 55 | use Apex\Cluster\Interfaces\{MessageRequestInterface, FeHandlerInterface}; 56 | class Profiles 57 | { 58 | 59 | /** 60 | * Create 61 | */ 62 | public function create(MessageRequestInterface $msg, FeHandlerInterface $handler) 63 | { 64 | 65 | // Get messge info 66 | list($var1, $var2, $var3) = $msg->getParams(); 67 | $request = $msg->getRequest(); 68 | 69 | // Get all form POST fields 70 | $post = $request['post']; 71 | 72 | // Create user 73 | $user = createUserModelObject(); 74 | 75 | // Return 76 | return $user; 77 | 78 | } 79 | 80 | /** 81 | * Load 82 | */ 83 | public function get(MessageRequestInterface $msg, FeHandlerInterface $handler) 84 | { 85 | 86 | // Get id# of user to retrieve 87 | $userid = $msg->getParams(); 88 | 89 | // Load user profile 90 | $user = loadUserModelObject(); 91 | 92 | // Return 93 | return $user; 94 | } 95 | } 96 | ~~~ 97 | 98 | In the above example, all messages sent to the routing key "users.profile.create" will invoke the above `create()` method, and all messages sent to "users.profile.load" will invoke the `load()` method. Again, these methods may return anything you wish as long as it can be serialized. Please see the [Message Requests](message_requests.md) page for details regarding the `MessageRequestInterface` object that is passed to each method. 99 | 100 | 101 | ## Message Preparation Handler 102 | 103 | Within the container definitions file (eg. ~/config/container.php) you may define a "prepare_msg_handler" closure, and if defined, this closure will be invoked against every incoming message received. Alternatively, you may also pass a closure as the third argument to the `Listener::listen()` method when starting a listener, which will override any closure defined within the container definitions file. 104 | 105 | This provides the ability to setup your specific environment as necessary for processing of incming messages, such as if you need to inject inputs into your specific framework, instantiate an object for the authenticated user, and things of that nature. 106 | 107 | 108 | -------------------------------------------------------------------------------- /docs/message_requests.md: -------------------------------------------------------------------------------- 1 | 2 | # Message Requests 3 | 4 | All messages dispatched must be an implementation of the `MessageRequestInterface` interface. Cluster does provide a default `MessageRequest` object, but it is somewhat expected you will copy and modify it as necessary for your own implementation, as you will see in the "Request Property" section below. 5 | 6 | 7 | ## Creating Messages 8 | 9 | Creating messages is very simplistic, and for example: 10 | 11 | ~~~php 12 | use APex\Cluster\Message\MessageRequest; 13 | 14 | $msg = new MessageRequest('shop.orders.add', $var1, $var2, $obj1, $obj2, $etc); 15 | ~~~ 16 | 17 | That's all there is to it. The constructor only requires one argument, being the routing key to dispatch the message to. Aside from the routing key, you may pass any and all additional parameters you wish as long as they can be serialized, which includes variables, arrays and objects. However, this does not include closures or resources. 18 | 19 | The listening consumer can then retrieve all parameters passed via the `getParams()` method of the message request object. 20 | 21 | 22 | ## Properties 23 | 24 | All message request objects have the following properties: 25 | 26 | Property | Type | Description 27 | ------------- |------------- |------------- 28 | `$msg_type` | string | The type of message. Will be either: rpc, queue, broadcast. 29 | `$instance_name` | string | The name of the server instance dispatching the message. 30 | `$caller` | array | The exact class / method name, file and line number from where the message was dispatched. 31 | `$request` | array | Standardized request information such as URI, IP address, post / get inputs, et al. See below for details. 32 | `$routing_key` | string | The routing key passed to the constructor. 33 | `$params` | iterable | All parameters passed to the constructor. 34 | 35 | 36 | ## Methods 37 | 38 | The message request object contains the following methods: 39 | 40 | Method | Description 41 | ------------- |------------- 42 | `GetParams():iterable` | Returns all parameters passed to the constructor. 43 | `getRequest():array` | Returns standardized request information. See the below section for details. 44 | `getCaller():array` | Returns a four element associative array that contains the class name, method name, filename, and line number the message was dispatched from. 45 | `getRoutingKey():string` | Returns the routing key passed to the constructor. 46 | `getType():string` | Returns the message type, will be one of: rpc, queue, broadcast 47 | `getInstanceName():string` | Returns the instance name of the dispatcher. 48 | `setType(string $type)` | Set the message type. 49 | `setInstanceName(string $name)` | Set the instance name of the dispatcher. 50 | 51 | 52 | ## Request Property 53 | 54 | Here in is the reason you may wish to write your own `MessageRequestInterface` class to use when dispatching messages. The contents of this array are defined within the class, and will be somewhat dependant on the framework / environment you're developing with. This array is meant to provide standardized information on each request to consumers, such as host, URI, authenticated user, post / get inputs, HTTP headers, et al. 55 | 56 | Although Cluster does provide a standard `$request` array, it's somewhat assumed you're developing with your framework of choice which provides its own input sanitization, user authentication, obtains the real IP address instead of `$_SERVER[REMOTE_ADDR]`, et al. To create your own implementation, simply copy the /src/message/MessageRequest.php file to your software, and modify the constructor to populate the `$request` array as desired. All objects created with the new class will continue to be dispatched and processed perfectly fine without issue. 57 | 58 | Nonetheless, if using the message request object provided by Cluster, the `$request` array will contain the following elements: 59 | 60 | Key | Type | Description 61 | ------------- |------------- |------------- 62 | mode | string | Either "http" or "cli", defining the mode of the dispatcher. 63 | host | string | The requested host (eg. domain.com). 64 | port | int | The requested port (ie. 443). 65 | uri | string | The requested URI 66 | method | string | The request method (eg. GET / POST). 67 | ip_address | string | Client's originating IP address. 68 | post | array | All $_POST variables sanitized via `filter_input_array()`. 69 | get | array | All $_GET variables sanitized via `filter_input_array()`. 70 | cookie | array | All $_COOKIE variables sanitized via `filter_input_array()`. 71 | server | array | All $_SERVER variables sanitized via `filter_input_array()`. 72 | http_headers | array | All HTTP headers as retrived by `getAllHeaders()` function if HTTP request, otherwise blank if CLI request. 73 | script_file | string | Only present for CLI requests, and is the filename of dispatching script. 74 | argv | iterable | Only present for CLI requests, and is the `$argv` array, or the command line arguments passed. 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/message_responses.md: -------------------------------------------------------------------------------- 1 | 2 | # Message Responses 3 | 4 | Every RPC call dispatched will return a `EventResponseInterface` object, and this class will never need to be instantiated by you. It contains all properties and methods of the `MessageRequestInterface` object, so please ensure to read the [Message Requests](message_requests.md) details for those. 5 | 6 | Aside from cloning the message request, this object also contains additional properties and methods as described below. 7 | 8 | 9 | ## Properties 10 | 11 | On top of the message request properties, the response object also contains the following: 12 | 13 | Property | Type | Description 14 | ------------- |------------- |------------- 15 | `$status` | int | The response status, mimics HTTP status codes (eg. 200 = ok, 505 = error, et al). 16 | `$consumer_name` | string | The name of the consuming server instance that processed the request. 17 | `$response` | array | The responses from each PHP class called. 18 | `$called` | array | An array of all PHP classes called during processing of the request. 19 | `$fe_handler` | FeHandlerInterface | The front-end handler, see [Front-End Handlers](fe_handlers.md) page for details. 20 | 21 | ## Methods 22 | 23 | On top of the message request methods, the response object also contains the following methods: 24 | 25 | Method | Description 26 | ------------- |------------- 27 | `getResponse(string $alias):mixed` | Returns the response from the PHP class with the corresponding `$alias`. 28 | `getAllResponses():array` | Returns an associative array of all responses, the keys being the aliases, and the values being what was returned by the corresponding PHP class. 29 | `getCalled():array` | Returns associative array of all PHP classes called, the keys being the response alias, and the values being the PHP class called. 30 | `getStatus():int` | Returns the status of the response. 31 | `getConsumerName():string` | Returns the name of the server instance who consumed the message. 32 | `setStatus(int $status)` | Set the status of the response. 33 | `setConsumerName(string $name)` | Set the name of the server instance consuming the message. 34 | `addResponse(string $alias, mixed $data)` | You will never need to call this directly, but sets an element within the `$response` property. 35 | `addCalled(string $alias, string $php_class)` | You will never need to call this directly, but will add an element to the `$called` array for which PHP class was called. 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/redis.md: -------------------------------------------------------------------------------- 1 | 2 | # Enable redis Auto-Loading 3 | 4 | Cluster includes support for redis allowing easy centralization of configuration across all server instances. When enabled, both the local container definitions file and YAML router file will be ignored, and instead will be loaded from redis, meaning when the configuration is updated on one server instance it will be updated across all server instances. 5 | 6 | 7 | ## Saving Configuration 8 | 9 | First you need to save the configuration into redis, which can be done via the `saveRedis()` method. Setup configuration as desired on one server instance, and once complete save the configuration to redis with the following example code: 10 | 11 | ~~~php 12 | use Apex\Cluster\Cluster; 13 | use redis 14 | 15 | // Connect to redis 16 | $redis = new redis(); 17 | $redis->connect('localhost', 6379); 18 | $redis->auth('my_password'); 19 | 20 | // Start cluster, and save to redis 21 | $cluster = new Cluster('app1'); 22 | $cluster->saveRedis($redis, true); 23 | 24 | echo "Configuration saved to redis.\n"; 25 | ~~~ 26 | 27 | Simple as that. Whatever the current configuration is at the time `saveRedis()` method is called, is the configuration that will be saved into redis and shared across all server instances. This includes the DI container file itself, all routes within the YAML configuration file, any routes you added via the PHP code, and all other configuration. 28 | 29 | The `true` boolean as the second argument if defined will send a broadcast message to all listeners prompting them to restart and reload configuration upon doing so. Please note, this is only applicable if the listeners are listening on the "broadcast" channel. 30 | 31 | 32 | ## Load Configuration 33 | 34 | Once configuration is saved into redis, loading it is as simple as passing the redis connection object as the second argument when instantiating Cluster. For example: 35 | 36 | ~~~php 37 | use Apex\Cluster\Cluster; 38 | use Apex\Cluster\Listener; 39 | use redis; 40 | 41 | // Connect to redis 42 | $redis = new redis(); 43 | $redis->connect('localhost', 6379); 44 | $redis->auth('my_password'); 45 | 46 | // Start cluster 47 | $cluster = new Cluster('app1', $redis); 48 | 49 | // Start listening 50 | $listener = new Listener(); 51 | $listener->listen(); 52 | ~~~ 53 | 54 | With the redis object being passed as the second argument, it will load all configuration from redis and ignore all local configuration. If the local DI container file is outdated, it will be overwritten by the contents of the DI container file within redis. 55 | 56 | Please note, when saving redis configuration do NOT pass the redis object to the constructor when instiantiating Cluster. Leave it as null, otherwise you will be saving an obsolete configuration. 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/router.md: -------------------------------------------------------------------------------- 1 | 2 | # Router Overview 3 | 4 | Being the main component, the router is where you define how incoming messages are routed, and which PHP classes are called. You can easily define routes within the PHP code using the `addRoute()` method or through an easy to follow YAML configuration file, auto-route messages based on the named routing key, audo-load router configuration from redis ensuring multiple server instances are always using the same configuration, and more. 5 | 6 | Additional pages on routing: 7 | 8 | 1. [Adding Routes in PHP](router_php.md) 9 | 2. [Router YAML Configuration File](router_yaml.md) 10 | 3. [Auto Routing](router_auto.md) 11 | 4. [Parameter Based Routing](router_params.md) 12 | 5. [Enable redis Autoloading](redis.md) 13 | 14 | 15 | ## Local Message Broker 16 | 17 | By default Cluster is configured to utilize the local message broker, meaning dispatching and consuming messages will work perfectly fine on one server instance without RabbitMQ installed. This allows you to develop the software with horizontal scaling fully implemented while running on one server instance, then as needed and when volume increases, easily split the system into multiple instances with RabbitMQ in the middle. You can easily switch from the local broker to RabbitMQ by modifying the `BrokerInterface::class` item within the container definitions file (eg. ~/config/container.php). 18 | 19 | ## Message Types 20 | 21 | Three types of messages are supported, which are: 22 | 23 | * rpc - Two-way RPC call where response is expected from the consumer. This is the default. 24 | * queue - A one-way message that is queued for processing, and only requires acknowledgement by message broker without providing a response. Used for resource intensive operations that don't require an immediate response, and can be processed either immediately if a consumer is listening, or fetched from queue later (ie. via crontab job). 25 | * broadcast - Broadcasts messages to all consumers listening. Useful for actions such as reload configuration, or any other action required by each individual consumer. 26 | 27 | 28 | ## Routing Keys 29 | 30 | Every message dispatched has a routing key assigned, which is a three segment string delimited by periods, such as "users.profile.create" for example. The three segments are broken up as follows: 31 | 32 | * First Segment - The overall package / module. 33 | * Second Segment - Class or subset name of message destination. 34 | * Third Segment - The name of the method within the PHP class that will be executed. 35 | 36 | The first two segments are simply identifiers to help keep routing keys readable and memorable, and can be routed to any PHP class(es) desired. The third segment however is the name of the method that will be called within each PHP class the message is routed to. 37 | 38 | Wildcards can be designated by using an asterisk such as "users.profile.*". Any segments left undefined will also be treated as wildcards, so "users.profile" is the same as "users.profile.*". You may also use "all" as a routing key which is the equivalent of specifying "*.*.*", and is useful for auto-routing. 39 | 40 | 41 | ## Routing Destinations 42 | 43 | All messages can be routed to one or more PHP classes, providing extensibility to the software by allowing a message to be processed by more than one PHP class. In the same vein, there can be multiple responses provided for each message, although only one marked as the "default" response. 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/router_auto.md: -------------------------------------------------------------------------------- 1 | 2 | # Auto-Routing 3 | 4 | Cluster allows messages to be auto-routed to different PHP classes based on the named routing key. Simply define a route to the routing key "all", and within the PHP class use merge fields surrounded by tildas ~, such as: 5 | 6 | > `App\~package.title~\~module.title~` 7 | 8 | For example, in the YAML router file you would use something such as: 9 | 10 | ~~~ 11 | rpc.catchall: 12 | type: rpc 13 | routing_keys: 14 | all: App\~package.title~\~module.title~ 15 | ~~~ 16 | 17 | With the above example, when a message is sent to the routing key "users.profile.delete" for example, the method at `App\Users\Profile::delete()` would be executed. 18 | 19 | ## Available Merge Fields 20 | 21 | All routing keys are comprised of three segments separated by periods: 22 | 23 | > PACKAGE.MODULE.METHOD 24 | 25 | All messages also are assigned a message type which can be either, rpc, queue, broadcast. The following merge fields are available in different naming conventions and can be used within any PHP class when defining routes: 26 | 27 | * ~package~ 28 | * ~package.title~ 29 | * ~package.camel~ 30 | * ~package.lower~ 31 | * ~module~ 32 | * ~module.title~ 33 | * ~module.camel~ 34 | * ~module.lower~ 35 | * ~method~ 36 | * ~method.title~ 37 | * ~method.camel~ 38 | * ~method.lower~ 39 | * ~msg_type~ 40 | * ~msg_type.title~ 41 | * ~msg_type.camel~ 42 | * ~msg_type.lower~ 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/router_params.md: -------------------------------------------------------------------------------- 1 | 2 | # Parameter Based Routing 3 | 4 | Cluster also supports parameter based routing meaning on top of matching the correct routing key, a message must also meet additional criteria. For example, if a RPC call is made with every template parsed you may wish a certain route only execute on a specific page with request method of POST, or only when the user is authenticated. 5 | 6 | 7 | ## Defining Routes with Parameters 8 | 9 | Parameter conditions are an associative array, the keys being the request key / field to check, and the value being the condition it must meet. If adding routes within PHP via the `addRoute()` variable you can simply pass the `$params` array as the sixth argument to the method, as described in [Adding Routes in PHP](router_php.md). 10 | 11 | Otherwise if defining routes via a YAML router file, you may define parameters within route definitions such as: 12 | 13 | ~~~ 14 | login_page: 15 | type: rpc 16 | params: 17 | method: "== POST" 18 | uri: "=~ login$" 19 | post.submit: "== login_user" 20 | routing_keys: 21 | twig.template.parse: App\Templates\Login 22 | ~~~ 23 | 24 | The above route will only accept messages if the request method is POST, the requested URI ends with "login", and the `$_POST[submit]` variable is equal to "login_user". 25 | 26 | 27 | ## Parameter Definitions 28 | 29 | The request keys / fields that may be checked are the same found within the `$request` property of the `MessageRequestInterface` object. Please see the [Message Requests](message_requests.md) page for details on elements found within this array. 30 | 31 | Any element within that array may be checked against. If the value of the element is n array itself, this can be designated by a period within the key. For example, assuming `$request[post]` is a sanitized `$_POST` array, and you wanted to check against the "action" form field, you would use "post.action" as the key. 32 | 33 | Parameter definitions are an associative array, the keys being the value to check against as described above, and the values are the conditions to check against. Each condition must begin with a two character operator, followed by the actual conditional string. Supported operators are: 34 | 35 | Operator | Description 36 | ------------- |------------- 37 | `==` | Equal to 38 | `!=` | Not equal to. 39 | `>=` | Greater than or equal to. 40 | `<=` | Less than or equal to. 41 | `=~` | Matches a regular expression. Note, any special characters including forward slashes must be escaped, and within YAML must be escaped with three backslashes (ie. \\\). 42 | `!~` | Does not match a regular expression. Note, any special characters including forward slashes must be escaped, and within YAML must be escaped with three backslashes (ie. \\\). 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/router_php.md: -------------------------------------------------------------------------------- 1 | 2 | # Adding Routes in PHP 3 | 4 | 5 | The main `Cluster` class extends the `Router` class, meaning all methods outlined below are available via the main `Cluster` object. 6 | 7 | ## Add Route 8 | 9 | Routes can be easily added via the `addRoute()` method. Although Cluster does allow multiple PHP methods to be executed for a single message to provide extensibility, this method only allows one PHP class to be defined at a time. If you need multiple PHP classes, call this method for each of them. 10 | 11 | **Parameters** 12 | 13 | Variable | required | Type | Description 14 | ------------- |------------- |------------- |------------- 15 | `$routing_key` | Yes | string | The routing key, 1 to 3 segments delimited by periods. Please see the [Router Overview](router.md) page for details on routing keys. You may use "all" to specify all keys. 16 | `$php_class` | Yes | string | The full PHP namespace / class that will be instantiated when a message matching the routing key is received. 17 | `$alias` | No | string | The alias the response from the PHP class will be stored in. Defaults to "default", but can differ if multiple PHP classes are being called for the same routing key. 18 | `$msg_type` | No | string | The message type to listen for. Supported values are: rpc, queue, broadcast. Defaults to "rpc". 19 | `$queue_name` | No | string | Only applicable if the message type is "queue", and is the name of the queue to create. This is the name you can fetch messages from using the `Cluster\Fetcher` class. 20 | `$params` | No | array | Used for parameter based routing, and is an associative array of the parameter conditions that must be met for the message to be accepted. Please see [Parameter Based Routing](router_params.md) page for details. 21 | `$instances` | No | array | The instance aliases to route incoming messages to. Only applicable if only select instances will process messages sent to this route, and defaults to "all" instances. 22 | `$is_config` | No | bool | Should always be left at false when creating via PHP with this method. 23 | 24 | 25 | #### Basic Example 26 | 27 | Here's a basic example of adding a route: 28 | 29 | ~~~php 30 | use Apex\Cluster\Cluster; 31 | 32 | $cluster = new Cluster('app1'); 33 | $cluster->addRoute('users.profile.*', App\Users\Profiles::class); 34 | ~~~ 35 | 36 | This adds a single route, so for example, when a message is sent to the routing key "users.profile.create", the receiving consumer will execute the `App\Users\Profiles::create()` method, and return the response. 37 | 38 | 39 | #### Multiple PHP Classes Example 40 | 41 | Extending on the above example, maybe the system has an additional package installed for managing user wallets, and upon loading a user's profile we want to retrieve both, the general profile plus the additional wallet information on the user. Below shows an example of this: 42 | 43 | ~~~php 44 | use Apex\Cluster\Cluster; 45 | 46 | $cluster = new Cluster('app1'); 47 | $cluster->addRoute('users.profile.*', App\Users\Profiles::class); 48 | $cluster->addRoute('users.profile.*', App\Wallets\Users::class, 'wallet'); 49 | ~~~ 50 | 51 | When a message is sent to the routing key "users.profile.load" for example, the method at `App\Users\Profiles::load()` will be called with the response added as the "default" response. The method at `App\Wallets\Users::load()` will also be called, with its response being stored as "wallet" within the message response. 52 | 53 | Here's an example of dispatching said message: 54 | 55 | ~~~php 56 | use Apex\Cluster\Dispatcher; 57 | use Apex\Cluster\Message\MessageRequest; 58 | 59 | // Dispatch message 60 | $dispatcher = new Dispatcher('web1'); 61 | $res = $dispatcher->dispatch(new MessageRequest('users.profile.load', $var1, $var2)); 62 | 63 | // Default response from App\Users\Profile::load() 64 | $profile = $res->getResponse(); 65 | 66 | // Response from App\Wallets\Users::load() 67 | $wallet_info = $res->getResponse('wallet'); 68 | ~~~ 69 | 70 | #### Specific Instance Examples 71 | 72 | Maybe you have several server instances and want only two of them to handle the processing of all image uploads. This can be accomplished with the following: 73 | 74 | ~~~php 75 | use Apex\Cluster\Cluster; 76 | 77 | $cluster = new Cluster('app1'); 78 | $cluster->addRoute('images.processor.upload', App\Gallery\Images::class, 'default', 'queue', 'uploaded_images', [], ['app3', 'app4']); 79 | ~~~ 80 | 81 | All messages sent to the routing key "images.processor.upload" will be sent only to the server instances "app3" and "app4" in round robin fashion, and will execute the method at `App\Gallery\Images::upload()`. 82 | 83 | ## Delete Routes 84 | 85 | There are two methods to delete routes. Upon adding a route via the `addRoute()` method, it will return a unique id# for that specific route. The specific route can later be deleted via the `deleteRouteId($id)` method, and for example: 86 | 87 | ~~~php 88 | use Apex\Cluster\Cluster; 89 | 90 | $cluster = new Cluster('app1'); 91 | $id = $cluster->addRoute('users.profile.*', App\Users\Profiles::class); 92 | 93 | // Delete route 94 | $cluster->deleteRouteId($id); 95 | ~~~ 96 | 97 | You may also use the `deleteRoutes()` method which will delete all routes matching a given criteria. This method accepts the following parameters, all of which are optional: 98 | 99 | Variable | Type | Description 100 | ------------- |------------- |------------- 101 | `$routing_key` | string | If specified, only routes matching this routing key will be deleted. 102 | `$php_class` | string | If specified, only routes matching this PHP class will be deleted. 103 | `$msg_type` | string | If specified, only routes matching this message type will be deleted. 104 | `$instance` | string | If specified, only routes matching this instance will be deleted. 105 | 106 | Please note, if more than one criteria is specified, only routes matching all criteria will be deleted. For example: 107 | 108 | ~~~php 109 | use Apex\Cluster\Cluster; 110 | 111 | $cluster = new Cluster('app1'); 112 | $cluster->add('users.profile.*', App\Profiles\Users::class); 113 | $cluster->addRoute('financial.orders.*', Shop\Orders::class); 114 | 115 | // Delete 116 | $cluster->deleteRoutes('users.profile.*'); 117 | ~~~ 118 | 119 | The above would delete the one route, leaving the rout with the routing key "shop.orders.*". 120 | 121 | 122 | ## Purge Routes 123 | 124 | If ever needed, you may purge all routes with the `purgeRoutes()` method. This will remove all routes leaving a blank router. 125 | 126 | ~~~php 127 | use Apex\Cluster\Cluster; 128 | 129 | $cluster = new Cluster('app1'); 130 | $cluster->purgeRoutes(); 131 | ~~~ 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /docs/router_yaml.md: -------------------------------------------------------------------------------- 1 | 2 | # Router YAML Configuration 3 | 4 | All routes can be defined via a YAML file, the default location of which is `~/config/router.yml`, but can be defined via the `$router_file` variable as the third argument when instantiating the `Cluster` class. Please view the /examples/yaml/ directory for example configuration files. 5 | 6 | The below table lists all supported root elements within the YAML file: 7 | 8 | Key | Description 9 | ------------- |------------- 10 | ignore | Optional boolean, and if set to false the file will be ignored and not loaded upon instantiation of Cluster. 11 | routes | All route definitions. See below for details. 12 | instances | Properties of specific instances. See below for details. 13 | 14 | 15 | ## routes 16 | 17 | The root `routes` element is an array of router definitions, defining exactly how and where incoming messages are routed. Each definition is an associative array which must have a unique top-level name, and supports the following elements: 18 | 19 | Element | Required | Type | Description 20 | ------------- |------------- |------------- |------------- 21 | type | Yes | string | The type of message, supported types are: rpc, queue, broadcast 22 | instances | No | string or array | Defines the specific instances messages will be routed to. This can be a string if there is only one instance, or a one-dimensional array if multiple instances. You may also prefix instances with a tilda ~ to exclude the instance (ie. all instances except the ones defined here). Defaults to "all" instances if undefined. 23 | params | No | array | Used for parameter based routing, and is an associative array of the conditions that must be met. Please see the [Parameter Based Routing](router_params.md) page for details. 24 | routing_keys | Yes | array | An associative array with the keys being the routing key and the values being the full PHP classes messages will be routed to. The PHP classes can either be a string for only one class, or if messages are routed to multiple PHP classes, an associative array with the keys being the response alias and the values being the full PHP class. See below for examples. 25 | 26 | 27 | #### Basic Example 28 | 29 | ~~~ 30 | rpc.default: 31 | type: rpc 32 | instances: all 33 | routing_keys: 34 | users.profile: App\Users\Profiles 35 | financial.orders | App\Shop\Orders 36 | all: App\CatchAll 37 | ~~~ 38 | 39 | Above is a standard RPC definition that does round robin to all server instances listening which contains two specific routes for the two specified routing keys, with a catch all added at the bottom for all incoming RPC calls that don't match either of the routes. 40 | 41 | 42 | #### Multiple PHP Classes Example 43 | 44 | ~~~ 45 | rpc.users: 46 | type: rpc 47 | instances: all 48 | routing_keys: 49 | users.profile: 50 | default: App\Users\Profiles 51 | wallet: App\Wallets\Users 52 | financial.orders | App\Shop\Orders 53 | all: App\CatchAll 54 | ~~~ 55 | 56 | Extending on the first example, above specifies two PHP classes for the "users.profile.*" routing key. Incoming messages to the routing key will call both PHP classes, and below is an example of how you'd read the response from a dispatched message: 57 | 58 | ~~~php 59 | use Apex\Cluster\Dispatcher; 60 | use Apex\Message\MessageRequest; 61 | 62 | // Set message 63 | $msg = new MessageRequest('users.profile.load', $var1, $var2); 64 | 65 | // Dispatch message 66 | $dispatcher = new Dispatcher('web1'); 67 | $res = $dispatcher->dispatch($msg); 68 | 69 | // Default response from Apex\Users\Profiles 70 | $profile = $res->get_response(); 71 | 72 | // Response from App\Wallets\Users 73 | $wallet_info = $res->getResponse('wallet'); 74 | ~~~ 75 | 76 | 77 | #### Specific Instance Example 78 | 79 | ~~~ 80 | 81 | images_uploaded: 82 | type: queue; 83 | instances: 84 | - app3 85 | - app4 86 | routing_keys: 87 | images.processor.upload: App\Gallery\Upload 88 | 89 | rpc.default: 90 | type: rpc 91 | instances: 92 | - ~app3 93 | - ~app4 94 | routing_keys: 95 | users.profile: 96 | default: App\Users\Profiles 97 | wallet: App\Wallets\Users 98 | financial.orders | App\Shop\Orders 99 | all: App\CatchAll 100 | ~~~ 101 | 102 | The above creates an "images_uploaded" queue, which is only processed by the consumer instances "app3" and "app4". The same RPC definition as above is also there, except modified to handle incoming messages on all listening consumers except for "app3" and "app4". 103 | 104 | 105 | #### Auto-Routing 106 | 107 | ~~~ 108 | rpc.users: 109 | type: rpc 110 | instances: all 111 | routing_keys: 112 | users.profile: 113 | default: App\Users\Profiles 114 | wallet: App\Wallets\Users 115 | financial.orders | App\Shop\Orders 116 | all: App\~package.title~\~module.title~ 117 | ~~~ 118 | 119 | Using the same RPC example, this route definition has been slightly modified with merge fields placed within the "all" routing key. These merge fields will be replaced by the named routing key, forced to titlecase. For example, a message sent to the routing key "supports.tickets.create" will call the PHP method at `App\Support\Tickets::create()` providing the ability to quickly develop out consumer methods without having to constantly keep the router configuration updated. 120 | 121 | 122 | ## instances 123 | 124 | Although mainly here for extensibility, the root `instances` element allows for per-instance properties to be defined. This element consists of associative arrays, each named the unique instance name. Currently, each instance supports the following properties; 125 | 126 | Key | Type | Description 127 | ------------- |------------- |------------- 128 | max_msg | int | Only applicable for instances that accept messages with "queue" type, and defines the maximum number of concurrent messages the instance will accept. Defaults to 1. 129 | 130 | For example: 131 | 132 | ~~~ 133 | instances: 134 | 135 | app3: 136 | max:msg: 5 137 | app4: 138 | max:msg: 5 139 | ~~~ 140 | 141 | With the above example, the instances "app3" and "app4" will only accept five concurrent messages at a time. 142 | 143 | -------------------------------------------------------------------------------- /examples/App/Shop/OrderModel.php: -------------------------------------------------------------------------------- 1 | id = $user->userid . '-' . rand(100000, 99999); 24 | } 25 | 26 | } 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/App/Shop/Orders.php: -------------------------------------------------------------------------------- 1 | getParams(); 28 | 29 | // Create order 30 | $order = new OrderModel($user, $product, (float) $amount); 31 | 32 | // Return 33 | return $order; 34 | } 35 | 36 | /** 37 | * Upload 38 | */ 39 | public function upload(MessageRequestInterface $msg, FeHandlerInterface $handler) 40 | { 41 | sleep(2); 42 | return true; 43 | } 44 | 45 | } 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/App/Shop/Users.php: -------------------------------------------------------------------------------- 1 | getParams(); 31 | 32 | // Create user 33 | $user = new UserModel($username, $username . '@domain.com'); 34 | 35 | // Create some orders 36 | $orders = [ 37 | new OrderModel($user, 'Dell Inspiron Laptop', 949.95), 38 | new OrderModel($user, 'Coffee Table', 89.90), 39 | new OrderModel($user, 'Mechanical Keyboard', 129.95) 40 | ]; 41 | 42 | // Return 43 | return $orders; 44 | 45 | } 46 | 47 | } 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/App/Users/Login.php: -------------------------------------------------------------------------------- 1 | getRequest(); 28 | 29 | // Return random number 30 | return bin2hex(random_bytes(8)); 31 | 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /examples/App/Users/Profiles.php: -------------------------------------------------------------------------------- 1 | getParams(); 28 | 29 | // Create user 30 | $user = new UserModel($user, $email); 31 | 32 | // Return 33 | return $user; 34 | } 35 | 36 | /** 37 | * Load 38 | */ 39 | public function load(MessageRequestInterface $msg, FeHandlerInterface $handler) { 40 | 41 | // Get params 42 | $username = $msg->getParams(); 43 | 44 | // Create user object 45 | $user = new UserModel($username, $username . '@domain.com'); 46 | 47 | // Return 48 | return $user; 49 | } 50 | 51 | 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /examples/App/Users/UserModel.php: -------------------------------------------------------------------------------- 1 | userid = rand(1000, 9999); 23 | } 24 | 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/App/Wallets/Bitcoin.php: -------------------------------------------------------------------------------- 1 | " where is one of the following: 12 | basic - Basic RPC Configuration 13 | multi - Multiple PHP Classes 14 | autorouting - Auto-Routing 15 | params - Parameter Based Routing 16 | queue - Fetch Messages from Queue 17 | instances - Specific Server Instances 18 | 19 | With each listener type there is a correspoding PHP file within this directory with the same name. For example, if you start a listener with "php listen.php basic", then you the basic.php script in this directory to run the example. 20 | 21 | Although not required, it's recommended you go through all examples in the order listed above as they expand on each other to cover all facets of Cluster. 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/autorouting.php: -------------------------------------------------------------------------------- 1 | dispatch($msg)->getResponse(); 20 | 21 | // Echo 22 | echo "GOt response of: ", $res, "\n"; 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/basic.php: -------------------------------------------------------------------------------- 1 | dispatch($msg)->getResponse(); 20 | 21 | // Echo 22 | echo "Created user ", $user->username, " who has the id# ", $user->userid, "\n"; 23 | 24 | 25 | // Add order to user, giving UserModel object in request, getting OrderModel object back. 26 | $msg = new MessageRequest('financial.orders.add', $user, 'iPhone', 889.95); 27 | $order = $dispatcher->dispatch($msg)->getResponse(); 28 | 29 | // Echo 30 | echo "Created order id# ", $order->id, " on user id# ", $user->userid, " for product: ", $order->product . " at amount ", $order->amount, "\n"; 31 | 32 | -------------------------------------------------------------------------------- /examples/init.php: -------------------------------------------------------------------------------- 1 | dispatch($msg)->getResponse(); 27 | 28 | // Echo 29 | echo "GOt response of: ", $res, "\n"; 30 | 31 | // Send queue message 32 | $msg = new MessageRequest('orders.images.upload', 'whatever'); 33 | $res = $dispatcher->dispatch($msg, 'queue'); 34 | 35 | 36 | echo "Both messages sent, which you should see within logging on the two listeners.\n"; 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/listen.php: -------------------------------------------------------------------------------- 1 | listen(); 30 | 31 | 32 | /** 33 | * Get listener configuration to use. 34 | */ 35 | function getListenerConfiguration():string 36 | { 37 | 38 | // Get command line arg 39 | global $argv; 40 | $config = $argv[1] ?? ''; 41 | 42 | // Check for invalid arg 43 | if ($config == '' || !file_exists(__DIR__ . '/yaml/' . $config . '.yml')) { 44 | echo "Invalid or no configuration specified. Please run 'php listen.php ' where < is one of the following:\n\n"; 45 | echo " [basic] - Basic RPC\n"; 46 | echo " [multi] - Multiple PHP Classes\n"; 47 | echo " [autorouting] - Auto-Routing\n"; 48 | echo " [params] - Parameter Based Routing\n"; 49 | echo " [queue] - Queue\n"; 50 | echo " [instances] - Specific Server Instances\n"; 51 | exit(0); 52 | } 53 | 54 | // Return 55 | return __DIR__ . '/yaml/' . $config . '.yml'; 56 | } 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /examples/multi.php: -------------------------------------------------------------------------------- 1 | dispatch($msg); 21 | 22 | // Get default UserModel object returned by App\Users\Profiles class. 23 | $user = $res->getResponse(); 24 | echo "Got user id# ", $user->userid, " with e-mail ", $user->email . "\n"; 25 | 26 | // Get array of OrderObjects returned by App\Shop\Users class. 27 | $orders = $res->getResponse('shop'); 28 | foreach ($orders as $order) { 29 | echo "Order ID# ", $order->id, " product: ", $order->product, " for ", $order->amount, "\n"; 30 | } 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/params.php: -------------------------------------------------------------------------------- 1 | dispatch($msg)->getResponse(); 19 | if ($res === null) { 20 | echo "Null response, as expected. Now let's modify the message...\n"; 21 | } else { 22 | echo "Got a response, routing successful -- $res\n"; 23 | } 24 | 25 | /** 26 | * Modify $_SERVER to match parameter criteria. Natrually, this would 27 | * be done automatically via other avenues such as your framework which automatically sanitizes the $_SERVER array. 28 | */ 29 | $_SERVER['REQUEST_METHOD'] = 'POST'; 30 | $_SERVER['REQUEST_URI'] = '/members/login'; 31 | 32 | 33 | // Send message again, this time expecting a response 34 | $msg = new MessageRequest('syrus.template.parse'); 35 | $res = $dispatcher->dispatch($msg)->getResponse(); 36 | if ($res === null) { 37 | echo "Received null response, did not route.\n"; 38 | } else { 39 | echo "Got a response, routing successful -- $res\n"; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /examples/queue.php: -------------------------------------------------------------------------------- 1 | fetch('image_upload'); 21 | 22 | // Wait for enter. 23 | echo "Ensure listener is off, and press Enter to dispatch messages to queue...\n"; 24 | readline(); 25 | 26 | // Dispatch messages 27 | $dispatcher = new Dispatcher('web1'); 28 | for ($x=1; $x <= 3; $x++) { 29 | $dispatcher->dispatch(new MessageRequest('orders.images.upload', "Image $x"), 'queue'); 30 | } 31 | 32 | // Wait again 33 | echo "Messages dispatched, press Enter to fetch them via Fetcher class...\n"; 34 | readline(); 35 | 36 | while ($msg = $fetcher->fetch('image_upload')) { 37 | echo "Got msg with params: " . $msg->getParams() . "\n"; 38 | } 39 | 40 | echo "All out of messages\n"; 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/yaml/autorouting.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Auto Routing Example 4 | # Extending on the multi example, this adds an additional "catch all" route that utilizes auto-routing. 5 | #################### 6 | 7 | 8 | routes: 9 | 10 | general.rpc: 11 | type: rpc 12 | routing_keys: 13 | users.profile: 14 | default: App\Users\Profiles 15 | shop: App\Shop\Users 16 | financial.orders: App\Shop\Orders 17 | all: App\~package.title~\~module.title~ 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/yaml/basic.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Basic Example 4 | # 5 | # Simple RPC configuration with two routes, and round robin to all listeners. 6 | #################### 7 | 8 | 9 | routes: 10 | 11 | general.rpc: 12 | type: rpc 13 | routing_keys: 14 | users.profile: App\Users\Profiles 15 | financial.orders: App\Shop\Orders 16 | 17 | -------------------------------------------------------------------------------- /examples/yaml/full.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Full Example 4 | # Includes all previous examples, plus a boradcast queue.#################### 5 | #################### 6 | 7 | routes: 8 | 9 | image_upload: 10 | type: queue 11 | instances: 12 | - app2 13 | routing_keys: 14 | orders.images.upload: App\Shop\Orders 15 | 16 | 17 | #################### 18 | # The below route will only accept messages that have a request method of POST, and 19 | # and URI ends with /login. 20 | #################### 21 | user_login: 22 | type: rpc 23 | instances: 24 | - ~app2 25 | params: 26 | method: "== POST" 27 | uri: "=~ \\/login$" 28 | routing_keys: 29 | syrus.template.parse: App\Users\Login 30 | 31 | 32 | general.rpc: 33 | type: rpc 34 | instances: 35 | - ~app2 36 | routing_keys: 37 | users.profile: 38 | default: App\Users\Profiles 39 | shop: App\Shop\Users 40 | financial.orders: App\Shop\Orders 41 | all: App\~package.title~\~module.title~ 42 | 43 | broadcast.cluster: 44 | type: broadcast 45 | instances: all 46 | routing_keys: 47 | cluster.sys: Apex\Cluster\Sys\Sys 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/yaml/instances.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Specific Instances Example 4 | # Extending on the queue example, the definitions below are modified so 5 | # only app2 accepts the queue messages, while app1 accepts the RPC messages. For this example 6 | # to work, you must spur two listeners with the commands: 7 | # 8 | # php listen.php instances app1 9 | # php listen.php instances app2 10 | #################### 11 | 12 | 13 | routes: 14 | 15 | image_upload: 16 | type: queue 17 | instances: 18 | - app2 19 | routing_keys: 20 | orders.images.upload: App\Shop\Orders 21 | 22 | 23 | #################### 24 | # The below route will only accept messages that have a request method of POST, and 25 | # and URI ends with /login. 26 | #################### 27 | user_login: 28 | type: rpc 29 | instances: 30 | - ~app2 31 | params: 32 | method: "== POST" 33 | uri: "=~ \\/login$" 34 | routing_keys: 35 | syrus.template.parse: App\Users\Login 36 | 37 | 38 | general.rpc: 39 | type: rpc 40 | instances: 41 | - ~app2 42 | routing_keys: 43 | users.profile: 44 | default: App\Users\Profiles 45 | shop: App\Shop\Users 46 | financial.orders: App\Shop\Orders 47 | all: App\~package.title~\~module.title~ 48 | 49 | 50 | 51 | # php listen.php app2 52 | -------------------------------------------------------------------------------- /examples/yaml/multi.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Multiple PHP Classes Example 4 | # Extending on the basic example, this modifies the "users.profile" routing key to route 5 | # messages to two different PHP classes. 6 | #################### 7 | 8 | 9 | routes: 10 | 11 | general.rpc: 12 | type: rpc 13 | routing_keys: 14 | users.profile: 15 | default: App\Users\Profiles 16 | shop: App\Shop\Users 17 | financial.orders: App\Shop\Orders 18 | 19 | -------------------------------------------------------------------------------- /examples/yaml/params.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Parameter Based Routing Example 4 | # Extending on the autorouting example, a new route is added that utilizes parameter based routing. 5 | #################### 6 | 7 | 8 | routes: 9 | 10 | #################### 11 | # The below route will only accept messages that have a request method of POST, and 12 | # and URI ends with /login. 13 | #################### 14 | user_login: 15 | type: rpc 16 | params: 17 | method: "== POST" 18 | uri: "=~ \\/login$" 19 | routing_keys: 20 | syrus.template.parse: App\Users\Login 21 | 22 | 23 | general.rpc: 24 | type: rpc 25 | routing_keys: 26 | users.profile: 27 | default: App\Users\Profiles 28 | shop: App\Shop\Users 29 | financial.orders: App\Shop\Orders 30 | all: App\~package.title~\~module.title~ 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/yaml/queue.yml: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | # Queue Example 4 | # Extending on the params example, a new definition is added as a ""queue" type. 5 | #################### 6 | 7 | 8 | routes: 9 | 10 | image_upload: 11 | type: queue 12 | routing_keys: 13 | orders.images.upload: App\Shop\Orders 14 | 15 | 16 | #################### 17 | # The below route will only accept messages that have a request method of POST, and 18 | # and URI ends with /login. 19 | #################### 20 | user_login: 21 | type: rpc 22 | params: 23 | method: "== POST" 24 | uri: "=~ \\/login$" 25 | routing_keys: 26 | syrus.template.parse: App\Users\Login 27 | 28 | 29 | general.rpc: 30 | type: rpc 31 | routing_keys: 32 | users.profile: 33 | default: App\Users\Profiles 34 | shop: App\Shop\Users 35 | financial.orders: App\Shop\Orders 36 | all: App\~package.title~\~module.title~ 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /signatures.json: -------------------------------------------------------------------------------- 1 | { 2 | "readme": "This file contains all digital signatures for this package created by Apex Signer (https:\/\/github.com\/apexpl\/signer\/), is automatically generated, and should not be manually modified.", 3 | "crt": "-----BEGIN CERTIFICATE-----\nMIIIRzCCBC+gAwIBAgIBADANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCQ0Ex\nGTAXBgNVBAgMEEJyaXRpc2ggQ29sdW1iaWExEjAQBgNVBAcMCVZhbmNvdXZlcjEj\nMCEGA1UECgwaQXBleCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxETAPBgNVBAsMCFN5\nc0FkbWluMRAwDgYDVQQDDAdjYUBhcGV4MSEwHwYJKoZIhvcNAQkBFhJzZWN1cml0\neUBhcGV4cGwuaW8xFDASBgNVHQ8MC2tleUNlcnRTaWduMB4XDTIxMDkzMDAzNDgy\nOFoXDTIxMDkzMDAzNDgyOFowgbgxCzAJBgNVBAYTAkNBMRkwFwYDVQQIDBBCcml0\naXNoIENvbHVtYmlhMRIwEAYDVQQHDAlWYW5jb3V2ZXIxFjAUBgNVBAoMDUFwZXgg\nU29mdHdhcmUxETAPBgNVBAsMCERldiBUZWFtMRIwEAYDVQQDDAlhcGV4QGFwZXgx\nIDAeBgkqhkiG9w0BCQEWEWNvbnRhY3RAYXBleHBsLmlvMRkwFwYDVR0PDBBEaWdp\ndGFsU2lnbmF0dXJlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA17EU\nnssjBNVM4c9CFLQACbBbQEeiWzgFVLV1GW4YrA0SlMQED1I\/7X4ugXj9vxh08tSO\njIQtSjGqDK8kBoi9uN3ZpYo1OnK9hbn2Jw9ZAZmxMqlJo6ZBDOCaFmDwPAEjBuUq\nFUe330FU4nhx7I2CjVOyooWjN8v88C+fBI0TMoilfXBlTNiwfeApZ6btoeRj0r1c\nu9AvGWzNPAm7xMWoP6jqRLywSSnjYBt3eZvXxzvZJ7carybt5\/HGxeDJh47Cy89A\nCZmwpHpNpk5fZ8VFRXaYNZvaPOLLDzjO\/CbZ21cazMr9x7vb1nxqXhucHO408lYC\nko7q0PexROh9hZbqKXmoRhc+2bAnStUpbVP6MTUQ2y323yVwQY8sjleJG6jrd0y3\nfPkDZZX\/kaAd\/+8I+f2pFsaUoAhWnCAolH6U8fheDwx+6eu5SuX4YB41GkYcnzb2\njIK4Qw5qKU9fmDnj1wz7\/heNAy+xmE7fzf2vy17IxXzjxMZNoTt051WnK9dSoGSt\nPeBpkS2FgPiH7GX4ImgksdUtslYCAV+DONlrPvb6tXXTw2tqWlOj6XtVTTPvRIOe\n4vldL4Sm7Ao9ddU6JLrlJJNcSCAAM50D+IxUfIzg5j\/VTyyfkvt40NTl5TuzN+iK\n69OKzHMcURLh9eI8PCpqLv2urmccv2ilzowTvO0CAwEAAaNTMFEwHQYDVR0OBBYE\nFCId6WLh3MD4qD\/88eJy+ivzlgg1MB8GA1UdIwQYMBaAFMlwBPz5LP7p7fl7ta2y\nVkC59ss1MA8GA1UdEwEB\/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggQBABjdKvn2\nEAiMw1yhj7q4WtSPmXgybQGNuhhuJP1wBkF8UU1qnLoWRafiouTIWedjVNJYRJLu\nmNZnhfk4YYSkCgT1fcburFt2fQIQGQcd4oSKSEYpxoYP99pG9wMfYWa91Kwgs6oa\nCKogb1aURzeai\/5RqmPwcI6kFT9A5xsKNwiIkkCu0ahCF9yOxCZ6Sz93eTMjdcLY\n50j4pqo7ekDyrJ6iZUOWu4xaKAYI8KQ90IOypu0YZWO4FEdgbOQ6D3OGMOCkdJoz\nwJMDDYdrNHGEfSYCWZ7oBjp6SuVhwY2WQT5bS1vt2fexTolMtQzQh0ScBSNZgUcM\nl3f1UzYLeiHWX9x2MTl2CoS5GRteUJsRXqnyw\/mfBNd3Ut\/ZMR0FZLkipICZTk5I\nFOvgO0ZSxrGNE7JJeZgej6xSd92tAblE1ZenVycp0S5J9VaWItZoxxUlDviVPRZC\nKB\/WBcfc9+1uNBevhS8vDIL9baZ7Ybaz9Srfyk9xbjxrNLqmnfRkBi0Im9DV6MjR\nrvwu7s06nJkPUHMJF2PlEBc0y5Oa7N8RTv3Vxvu3+vGAgdlFlifHAhOyxpdOThwU\nrsh8EvbUm9+TR6Gc1r+dcDspuWj4ZQlCpX6Bz08UdBq95Obfa\/euyrVpEImvdK3p\nvpFY851NZ058QGXSYpPYj1Zf0zWlrRy7aWOSTvCZGjU++phrp2YargF5z9sX8U0\/\nssW53tB2rJo\/dTXdfS1Ov4ycUzknr3AOVnEHbpM1JM2qMpyFavjQpfRY\/g92FHXv\nyCEhdPkY39i4RPP4o06Ppm6GXXnt5zoR6jxMUPTzkglaQqZAPA03c9uXGg0LrLVk\nehOU6BKLCcnDpLSjTxxoFrFfz+6nmaMeas4SuN+BgiZogGg3gxdjDdhZ3vDKlkR2\n\/t2nMjzfR2i68XMmLffGnlN7cZZfYoC4PYeiQFRMbho6BZCN\/ns5IP0TS1jElpVx\nhlFj5tFxV4oCtRyTEASuORXSl2u9JcohSIL4++igXhGarynxCxqpBTv6CwzNAUgl\nr3M22WMvPgtBakaQKu6Rr34pw9rOZLX8t3+Xu6BZHt3Q3cnvvRzfDqUbyJI+5DBn\njHG9MkGDEAI2Lg18eGpSKEKzKEVuLvdJE+agWTmrqF23e9Djd4glS3Y7kFckpJiT\nHzWLdmlrvWGWbqpL\/NYRmTCK3dgaKUbfnfvwEII0qVwjFuROG20J8rDCjBHZgcYT\npyV2pOqKkssIzyKCGUiU1uP6xPG4MjULUWkiZwzOMeYv0toxULSaWygPzIZZJ95L\nYXjZkXkrAe+hwhCiBgEAfJSrjG\/Iy9W7Whjb19vb1E\/0au2LHVciOh0Fu1WlRQKj\nwUHkt8iDpcZrwNs=\n-----END CERTIFICATE-----\n\n\n", 4 | "latest": "2.0.3", 5 | "releases": { 6 | "2.0": { 7 | "timestamp": 1643582852, 8 | "merkle_root": "953bb23a5da40692ff07c90d6c4bc950e25d0ea49ab12ac30ec1671c8f63ff8d", 9 | "prev_merkle_root": "", 10 | "signature": "07a8a596d7569b23c9d90228705997b6277e0d6943a7f184f48894b44b2b7b2ea00ddabc636aa3d56d934df2c3a3e47cd9a96df531a5781175f2f6ee936f50a11ad0b11afad66e60d2e6b8391a7b5d046783544076346580c2257f649df95a53f82d56c6028c5b7a201d2752641bd82247d2936e93db5251129b39f2d4ca0d1d94e84da550efeafec2441a68128f74272275cf123485e2aaebb0b90e9057a9620a13f6db022e8337330edda1190ceb5157d439a6012f6856d4e7b447c7a3cc0dc9d1ff5eddb3e76de4433c5f9f581069642a558903653d436a83d061ed046cf439c88a645a6f30e786fc225174dca1a3af1234ead40d6dd45299721697102d6ddae686becde200cce980554f617af5172cc378eaf0189e33773b19e970741925019e535739a41c1f9789c85410f62f2dda6161343ecca2683bfdcf232a2f53c61c4e18d17374b14f723a04cddb1b57eb425f68d05a79c6ee543d935b4ab3e18de0369faf2a4c419fe7c9504a5c823db1de2ae21f4c1ccaf0a46a5a3a2431231c21ea8b49988e58de1b701a5238fff7df882e85c69da13d7317b4b6864cd686293ba0ef83cde86977e3882f8d91c55a8e6374b02a2c325532f19b37bc1642fc03a8726e155f8c0a082cdd918336f5aa14a9d2cd3ef9b029ff2e70ad4d3e744e649be12772a62e527ecb91dfe586d26745141716ca280a0f41f9ffdcbd19f755c2" 11 | }, 12 | "2.0.1": { 13 | "timestamp": 1673561175, 14 | "merkle_root": "778d29243e8ffc804929068f4623e70ac96831e96169c910ba0928645f66e386", 15 | "prev_merkle_root": "953bb23a5da40692ff07c90d6c4bc950e25d0ea49ab12ac30ec1671c8f63ff8d", 16 | "signature": "c5e64dbbb5ec0f065a20d3ade4623d31b10aa24eb559666f3dce34a496976307cf5e98f055d7794c284a26aa6b9df67b4817f2771e20f56607fd89bbe157c8e66cd41132f56c5140ae71a9a956f08aa8f605fecb4652d223ab065070a78f3d3a0747ff51edda55aef838dd97a0b53e79dd2208d98bd2684e62c7f22ba266da391b65b410b265d553ca5e94ebeae70b6268aab76f02d641b74369f887bfc74c88d4762e56254b51659b75e7caa259a2008f9776af12db5f2f5e7283ccc8fd8fa98ce7b40a4fd46e9d45d88cefb5a36e19113956fe380cab0555b35b6dde0fb5f7688112f575d8da819fc6a1cb90a03f3ec52a13ed0f8b67a95baa3562abcf0a2f5764546a52c8bca8eadd892e697f873aabf56be0caede238e445961cedc172c5a8df4ee638a87dba39bb0a76f4c40bf42805e8f322f55ee9eeca3e770e4b2b3ff6d68e688d64230627be43b98d9bc26d3b4ec610dfb4fe370fb903a0351ec9ad2f2925d74bcb510f9bee0da853e67eced1baf71124650be27029116fcad35a0ca5112545da2de71f4ce1307d066d1e3df98ab51cabade8b6b523bb341ce3e7a698cf04e68a29a7398e0b285ced177813df9a7c2f579b6d86092bf258ee9c605032c26006e6503ee91c588f82a85ab32257e853e36f95dbe8aa3b8ec2810fcad8809ac55dc7628d71a9f0adffa0cb2af517ca6dd53d202c448a7fcb68c0c3646c" 17 | }, 18 | "2.0.2": { 19 | "timestamp": 1684755990, 20 | "merkle_root": "9152e5767109a73df6916cb47644253eae3c7b12572f1f115a9554d216dd69e9", 21 | "prev_merkle_root": "778d29243e8ffc804929068f4623e70ac96831e96169c910ba0928645f66e386", 22 | "signature": "420479804a286b5407789e85c99042a7db9b47bb05d2aa8a44076915314b97eaf015e6b6f936d7f3bb873cc53c805330b54ff28f825fe4186eb48d2e292fd33cd941c54040c942fc19ed12543c8331bb75ac36719fbfe1b1febe13c903454c382a279c2ec6682376a3a74d026a6334e8bcedd74550ec8dc1fcbffe48381913a202f9065a09985bb0ef553c46331ac11769b3286b10807af984558f35a07959d22361f3e1159088e55d1244d8853f3f6c9bffca5791db3a844f6f0c970b8bcf05e28695a1d63506456b1a31505bb99f6388a2e8ee0fccaa542d0e3b0fc5db3ee92fa607c42470a3546f56ddbfd3402f69f3adc8d2ec36957551c1b76fd21a49eb7cf36556ba241bbc22fbeba13e0855a206db137ed3b20c8b96d31b4ed8822d6c1eec1f088992e756e5a642ac426dfb90f6a9d6c47a9735cfd215f1b7710e128c955eefcfdb2a5a9b5523642798c7c9f303709ffb0774e0145daa449b6ace3f8d9b485c30378e59b761347f48232fad416ba351c8cb120849479a1f8c3a2e21693525f7a86e24b21c8d4c05be7dd26431548a36f32c1d3dbc6b0018863cec8a8bd6303fcd2f9a5ecc1d8039777354a533f5d6aa138483c20e0c1d6149e0270cd5b7e5b4f26b19dc5c68ff68e029dd42bd82fae232e45695562f6d0254eb4a31fd30d11df71278583fd6e58f308e26302d9b6690bd93b876ac8b75f5c20e99ace8" 23 | }, 24 | "2.0.3": { 25 | "timestamp": 1697788900, 26 | "merkle_root": "f07a0e9a0390f3c77751d4881ed0185de8563cd25b76e85ccbab4bd1f77a372b", 27 | "prev_merkle_root": "9152e5767109a73df6916cb47644253eae3c7b12572f1f115a9554d216dd69e9", 28 | "signature": "c191794e7b4cb1048ba79161bb37de42d79ebbfe9144cce53526b4533548d63623a32211de6dc71f4834febee3aa5791dd2f3b54ed7bf8922d1e400514a09609a182f238755c1d30cef10cfc6b8a5401fcf566a743662e25701c716b3ddbf131505796a69d39ab8dfa8f8d68f1b76c27fd86953e2488af226e837cd4ecc34d58dfc9169a9528ac4ff37fc27fb7837f73a4970b67357fa7743ab36ad5651dfca9ce7adf811cbd409d55d4780d9502724aefc08c1e1a2ffe4d5f8f57af24daa898cfeee0f03b711f04b8852a2838e672e85a946e599a42ab302fc8d9b76d2717c72e5db27927c4d5c4a657e466a6f1bdaa22905ffe54fb7901a008f1680ad7f81d4f6fd640d59e19da059eddd5a7d5655d1700d18eb3220367e529a48fe1d3ea1649fff743ad5f5750c3b59bf824014496eda740864a3868c42d877a4865be710465a5368ea480e57c68b1c205c0af91b9275c55909dd041acfedd52aa4ff27ba6b88b0f4564bc1b4da1f6a2ccda3aebf740f2bad6823995da219c98f7e785d1259857fd288a042bdb7f316b2f39d47671b7bf274d7a4da4ca8186158fc47dec72fe8eae40c32e2678980de36682242484bea0c48b5086d3ade670a6f5666b40f14de962dd3479f14defb5ab811aff91f858146b142e58b8a71e12c66daba7a37da57bccd51bca58a1f192a6abda10a7002da841332b8d3aa4782268167d51e40e" 29 | } 30 | }, 31 | "inventory": { 32 | "License.txt": "8ed904508e133df5b0abf709c53a044d48f67e18", 33 | "Readme.md": "4086998aa63ba04a43342f8dafed2cd2601ffd99", 34 | "composer.json": "02d83c49b6e5331057f9069b48b7fd2644ba1a0c", 35 | "config\/container.php": "88e3d92e0972fcbb51501898363d1bf7986ee656", 36 | "config\/router.yaml": "b20dec4358d531d1daa226e0cc40ed30d2c12973", 37 | "contrib.md": "39372e097735fdb716b6f5b8f959f5aa985cea9d", 38 | "docker-compose.yml": "918c34c0e2da4293037493313e0b59d7dbceec63", 39 | "docs\/cluster.md": "6763cd143a49fc1b9771f83f50287ad60291ef2e", 40 | "docs\/dispatch.md": "8472e8356b82cfb4ed205ec8d60483ce88ccb221", 41 | "docs\/fe_handlers.md": "966a52553250e329374b7a2bdfb603b8e9b524ce", 42 | "docs\/fetch.md": "24dfe6ea0aa201108425de1970266fafdf381aed", 43 | "docs\/index.md": "4086998aa63ba04a43342f8dafed2cd2601ffd99", 44 | "docs\/listen.md": "a60a27e1575d94da097961a0e878f5bdb0305cc6", 45 | "docs\/message_requests.md": "412c1d0f722e7171c9edce8530d216e9782f2b21", 46 | "docs\/message_responses.md": "f31eb9c28d94e7af425b2d9fc16936c8ba591d17", 47 | "docs\/redis.md": "7c3e2cb4b29af5d26f96ca5f0a69d29ae0c8d664", 48 | "docs\/router.md": "f33bb6eba03b970c1cc61205523ab2adca46b217", 49 | "docs\/router_auto.md": "faa3f473b34e7b79827a521b7b4b343438aadb25", 50 | "docs\/router_params.md": "40c28be81653cde7b329f970dc09910b88a73896", 51 | "docs\/router_php.md": "3af4d2940435cb8deace71e9aaab17100211fd26", 52 | "docs\/router_yaml.md": "8bd24eceec70dd8f44855bdb9bb2b8cce9cb37fe", 53 | "examples\/App\/Shop\/OrderModel.php": "856f2a00bd9176f44039c4589548384d951610d4", 54 | "examples\/App\/Shop\/Orders.php": "098c8646752de26274acb2db43c60b3e0572a488", 55 | "examples\/App\/Shop\/Users.php": "5e684400b38f7a50269cbb6548715dcf0355aa41", 56 | "examples\/App\/Users\/Login.php": "2e7ec0ccb612f89cfffd28d4b6e805bed01c2ee8", 57 | "examples\/App\/Users\/Profiles.php": "3900348e98b30b449d3d659c1821157fd00fde9e", 58 | "examples\/App\/Users\/UserModel.php": "b2268d8f8c1299ded40cb871837df2555f06ec70", 59 | "examples\/App\/Wallets\/Bitcoin.php": "16b9c8a7c825fbaef7eee9bfba685c49ec0a3d20", 60 | "examples\/Readme.md": "11a0d617a1a7a770007ebbb64cbdfac64b9aa255", 61 | "examples\/autorouting.php": "5a453c2401c57434bd5adb6a9da1e4d44748eac6", 62 | "examples\/basic.php": "899c8cbbdc4ba4e0ee33f8c99fa6950261d5c35f", 63 | "examples\/init.php": "7945fc2c6abaef7296d83e149042393dc404a4ec", 64 | "examples\/instances.php": "d710592d7432ae60b2f71f1651a6863c8650f19c", 65 | "examples\/listen.php": "224c787ff1fa266f1feebb66433f7b5baf43fa3b", 66 | "examples\/multi.php": "d32e28608e6338c367a9452190337cc629c0f6eb", 67 | "examples\/params.php": "42a3514e49840094e3bf8ace0c72d5c396444215", 68 | "examples\/queue.php": "8cf8d879b0cbe15ad456a1aa427edc6813dc711b", 69 | "examples\/yaml\/autorouting.yml": "9bf45410d25c1deadb2194acf33e822992581c4a", 70 | "examples\/yaml\/basic.yml": "1d82da65a79804f3f447d42c4dc5f900a2c5d9ff", 71 | "examples\/yaml\/full.yml": "bbe404edb2a5eca486e07113b5d94c799edf5f21", 72 | "examples\/yaml\/instances.yml": "a51ad3cb17ef01911697b33feb2ddc46207b527b", 73 | "examples\/yaml\/multi.yml": "b4a72b8ff7ec7ea1ae11b4a8063cd51913d57f55", 74 | "examples\/yaml\/params.yml": "3118c7495773519f7e00d17962563c0b0df090c9", 75 | "examples\/yaml\/queue.yml": "309bcb7b74e942c82ec4582cace51a7ad61e64a2", 76 | "phpunit.xml": "79d27547d5a36953c16de2577836eb36118ee2fd", 77 | "src\/Brokers\/Local.php": "fd97dcada0890d94cbd05ef70eb1ab9abb29e410", 78 | "src\/Brokers\/RabbitMQ.php": "74cf620f600eefd27e98b33c52e011b3946911af", 79 | "src\/Cluster.php": "ba236317d9858d07018bf7379f29082757ba0021", 80 | "src\/Dispatcher.php": "4dec1f909b215506ce9520ef68a538495d827df4", 81 | "src\/Exceptions\/ClusterDuplicateRoutingAliasException.php": "4982a941523f14c6b3bb7659f3a092549b3c87c6", 82 | "src\/Exceptions\/ClusterExchangeTypeOverlayException.php": "e833b95a2104b75b6b95edf9de34f66845031058", 83 | "src\/Exceptions\/ClusterFileNotWriteableException.php": "98ee72328938cd7a9b7027700fa5fd772de61483", 84 | "src\/Exceptions\/ClusterInvalidArgumentException.php": "8b041db38f43b19209a25f71afd816f84942fa1c", 85 | "src\/Exceptions\/ClusterInvalidParamOperatorException.php": "8c6750790ffbf90e576db5b405e342dc8728053c", 86 | "src\/Exceptions\/ClusterInvalidRoutingKeyException.php": "87be4d994c9889fdd37bc3f9f4633575e2e8ac03", 87 | "src\/Exceptions\/ClusterOutOfBountsException.php": "e84d68ba5059a3e96b4a32f82be589ec1d8f3835", 88 | "src\/Exceptions\/ClusterParamValueNotExistsException.php": "9bb45dd541e8e65bddf635aec76f2e75ac21236f", 89 | "src\/Exceptions\/ClusterPhpClassNotExistsException.php": "3c6b0673808529040d65071f74627452557f8726", 90 | "src\/Exceptions\/ClusterTimeoutException.php": "b294360046c21512fa9b3b9b6f4d323b4d60f22e", 91 | "src\/Exceptions\/ClusterYamlConfigException.php": "8127d9904f078bfca2bcca2db0f662cb5c716455", 92 | "src\/Exceptions\/ClusterZeroRoutesException.php": "486d0ce0f23284d6d295501999fa30cc89f9c4a0", 93 | "src\/FeHandlers\/Generic.php": "01db40ae823d4fc4a6249960440cc4b8cf9addd6", 94 | "src\/FeHandlers\/Syrus.php": "f5f590a9bf888f8414b4a4048eabb2f0ff035fbb", 95 | "src\/Fetcher.php": "aaabc1bf90aafca6a23435c8c37188e2545d5d2b", 96 | "src\/Interfaces\/BrokerInterface.php": "4641fc469c31e71eca48a167451b97a864b25fd0", 97 | "src\/Interfaces\/FeHandlerInterface.php": "37ff48806f037e1b819ed00140d642b5b4788f30", 98 | "src\/Interfaces\/MessageRequestInterface.php": "e0bf95986795e59ce21b5b09246298b94adae20e", 99 | "src\/Interfaces\/MessageResponseInterface.php": "1caff0443344ab625c3fceca115ecb34a8bdec8a", 100 | "src\/Interfaces\/ReceiverInterface.php": "4d9ef75465e07cd8f2007b32055853355c700616", 101 | "src\/Listener.php": "59dc956d36d6d80bf8a277507af74d00f3d205cc", 102 | "src\/Message\/MessageRequest.php": "93a2dca5667e92ec286b57b675f9b9767d773bb1", 103 | "src\/Message\/MessageResponse.php": "52670277c50d3565fce3980f17d4725ae1f4c01e", 104 | "src\/Receiver.php": "7c21ad412d1478e839e877511d6f179f4ec2661e", 105 | "src\/Router\/InstanceMap.php": "fb8c7606566f8dd0437f6713bb961de8b4ad13ee", 106 | "src\/Router\/Loaders\/RedisLoader.php": "fef84b0c89aba312db0a1411517b93d634667c7f", 107 | "src\/Router\/Loaders\/YamlLoader.php": "08d05145973b8d58ab3419f1da608c68c1221478", 108 | "src\/Router\/Mapper.php": "d1ee380284e7ce1ca4ece4a8acefa580f5726e09", 109 | "src\/Router\/ParamChecker.php": "6733134467085d5175caa276aa6233eca0ac700d", 110 | "src\/Router\/Router.php": "62906568f3f376f1fa2d143596cd51fd99973fa3", 111 | "src\/Router\/Validator.php": "80eebe40c20765e10debdda7a6b3ae54f390b6da", 112 | "src\/Sys\/Sys.php": "ada85d56ffde5bcb7350a3965d99532950e78cfe", 113 | "tests\/router_test.php": "d3e513cfa8f02e50eee81cd1f3bfd3e73895e422", 114 | "tests\/yaml_loader_test.php": "0371dbc0a7a8eac4415d15ee1b7fbc63ec8dda52" 115 | } 116 | } -------------------------------------------------------------------------------- /src/Brokers/Local.php: -------------------------------------------------------------------------------- 1 | getRoutesMap($cluster->instance_name); 32 | if (count($map->getAllRoutes()) == 0) { 33 | //throw new ClusterZeroRoutesException("There are no routes configured to listen to msg_type $msg_type"); 34 | } 35 | $this->receiver = Di::make(ReceiverInterface::class, ['map' => $map]); 36 | 37 | } 38 | 39 | /** 40 | * Publish message 41 | */ 42 | public function publish(MessageRequestInterface $msg):?MessageResponseInterface 43 | { 44 | return $this->receiver->receive($msg); 45 | } 46 | 47 | /** 48 | * Extract MessageRequestInterface object from body of message received by listener / consumer. 49 | */ 50 | public function extractMessage($msg):MessageRequestInterface { 51 | return $msg; 52 | } 53 | 54 | /** 55 | * Reply to RPC call. 56 | */ 57 | public function reply($data, $msg) { 58 | return $data; 59 | } 60 | 61 | /** 62 | * All methods below this line are nulled, not required for this 63 | * class and only present to satisfy the requirements of the BrokerInterface. 64 | */ 65 | public function openChannel() { } 66 | public function closeChannel():void { } 67 | public function declareExchange(string $name, string $type, bool $durable, bool $auto_delete):void { } 68 | public function deleteExchange(string $name):void { } 69 | public function declareQueue(string $name = '', bool $durable = false, bool $exclusive = false, bool $auto_delete = true):string { return $name; } 70 | public function bindQueue(string $queue, string $exchange, string $routing_key):void { } 71 | public function unbindQueue(string $queue, string $exchange, string $routing_key = ''):void { } 72 | public function deleteQueue(string $name):void { } 73 | public function purgeQueue(string $name):void { } 74 | public function consume(string $queue = '', bool $no_ack = false, bool $exclusive = false, callable $callback = null, int $max_msg = 1):void { } 75 | public function wait(bool $is_rpc_dispatcher = false, int $timeout = 0):void { } 76 | public function ack($msg):void { } 77 | public function nack($msg):void { } 78 | public function fetch (string $queue, bool $no_ack = false):?MessageRequestInterface { return null; } 79 | 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/Brokers/RabbitMQ.php: -------------------------------------------------------------------------------- 1 | host = 'localhost'; } 44 | if ($port == 0) { $this->port = 5672; } 45 | if ($username == '') { $this->username = 'guest'; } 46 | if ($this->password == '') { $this->password = 'guest'; } 47 | 48 | } 49 | 50 | /** 51 | * Open connection to RabbitMQ 52 | */ 53 | public function openChannel():object 54 | { 55 | 56 | // Check if already connected 57 | if ($this->channel !== null) { 58 | return $this->channel; 59 | } 60 | 61 | // Try to connect 62 | try { 63 | $this->conn = new AMQPStreamConnection($this->host, $this->port, $this->username, $this->password); 64 | } catch (AMQPConnectionClosedException $e) { 65 | throw new \Exception('Unable to connect to RabbitMQ'); 66 | } 67 | 68 | // Return 69 | $this->channel = $this->conn->channel(); 70 | return $this->channel; 71 | } 72 | 73 | 74 | /** 75 | * Close connection 76 | */ 77 | public function closeChannel():void 78 | { 79 | $this->channel->close(); 80 | $this->conn->close(); 81 | } 82 | 83 | 84 | /** 85 | * Declare an exchange. 86 | */ 87 | public function declareExchange(string $name, string $type = 'direct', bool $durable = false, bool $auto_delete = true):void 88 | { 89 | $this->channel->exchange_declare($name, $type, false, $durable, $auto_delete); 90 | } 91 | 92 | /** 93 | * Delete exchange 94 | */ 95 | public function deleteExchange(string $name):void 96 | { 97 | $this->channel->exchange_delete($name); 98 | } 99 | 100 | /** 101 | * Declare queue 102 | */ 103 | public function declareQueue(string $name = '', bool $durable = false, bool $exclusive = false, bool $auto_delete = true):string 104 | { 105 | list($queue, $err) = $this->channel->queue_declare($name, false, $durable, $exclusive, $auto_delete); 106 | return $queue; 107 | } 108 | 109 | /** 110 | * Bind queue to an exchange. 111 | */ 112 | public function bindQueue(string $queue, string $exchange, string $routing_key = ''):void 113 | { 114 | $this->channel->queue_bind($queue, $exchange, $routing_key); 115 | } 116 | 117 | 118 | /** 119 | * Unbind queue from an exchange. 120 | */ 121 | public function unbindQueue(string $queue, string $exchange, string $routing_key = ''):void 122 | { 123 | $this->channel->queue_unbind($queue, $exchange, $routing_key); 124 | } 125 | 126 | /** 127 | * Delete queue 128 | */ 129 | public function deleteQueue(string $name):void 130 | { 131 | $this->channel->queue_delete($name); 132 | } 133 | 134 | /** 135 | * Purge queue 136 | */ 137 | public function purgeQueue(string $name):void 138 | { 139 | $this->channel->queue_purge($name); 140 | } 141 | 142 | 143 | /** 144 | * Consume 145 | */ 146 | public function consume(string $queue, bool $no_ack = false, bool $exclusive = false, callable $callback = null, int $max_msg = 1):void 147 | { 148 | 149 | // One message at a time, if needed 150 | if ($no_ack === false) { 151 | $this->channel->basic_qos(null, $max_msg, null); 152 | } 153 | 154 | // Consume 155 | $this->channel->basic_consume($queue, '', false, $no_ack, $exclusive, false, $callback); 156 | } 157 | 158 | /** 159 | * Wait 160 | */ 161 | public function wait(bool $is_rpc_dispatcher = false, int $timeout = 5):void 162 | { 163 | 164 | // If RPC dispatcher 165 | if ($is_rpc_dispatcher === true) { 166 | $this->channel->wait(null, false, $timeout); 167 | return; 168 | } 169 | 170 | // Wait for connections 171 | while ($this->channel->is_open()) { 172 | $this->channel->wait(); 173 | } 174 | 175 | } 176 | 177 | /** 178 | * Publish message 179 | */ 180 | public function publish(MessageRequestInterface $msg):?MessageResponseInterface 181 | { 182 | 183 | // Get msg_type 184 | $msg_type = $msg->getType(); 185 | $routing_key = $msg->getRoutingKey(); 186 | $ex_name = 'cluster.ex.' . $msg_type; 187 | 188 | // Declare callback queue, if RPC 189 | if ($msg_type == 'rpc') { 190 | $this->callback_queue = $this->declareQueue('', false, true, true); 191 | $this->consume($this->callback_queue, false, false, [$this, 'onResponse']); 192 | } 193 | $this->correlation_id = uniqid(); 194 | 195 | // Define message properties 196 | $msg_properties = [ 197 | 'delivery_mode' => $msg_type == 'ack_only' ? 2 : 1, 198 | 'content_type' => 'text/plain', 199 | 'timestamp' => time(), 200 | 'type' => $msg_type, 201 | 'app_id' => 'Apex/Cluster', 202 | 'cluster_id' => $msg->getInstanceName(), 203 | 'correlation_id' => $this->correlation_id 204 | ]; 205 | 206 | // Add reply-to if RPC call 207 | if ($msg_type == 'rpc') { 208 | $msg_properties['reply_to'] = $this->callback_queue; 209 | } 210 | 211 | // Get message 212 | $payload = new AMQPMessage( 213 | serialize($msg), 214 | $msg_properties 215 | ); 216 | 217 | // Publish message 218 | $this->channel->basic_publish($payload, $ex_name, $routing_key); 219 | if ($msg_type != 'rpc') { 220 | return null; 221 | } 222 | 223 | // Wait for response, if RPC call 224 | try { 225 | $secs = Di::get('cluster.timeout_seconds') ?? 5; 226 | $this->wait(true, (int) $secs); 227 | } catch (AMQPTimeoutException $e) { 228 | 229 | // Close connection 230 | $this->closeChannel(); 231 | 232 | // Add log 233 | $cluster = Di::get(Cluster::class); 234 | $cluster->addLog("RPC timeout with routing key: " . $msg->getRoutingKey(), 'warning'); 235 | 236 | if (Di::has('cluster.timeout_handler')) { 237 | Di::call('cluster.timeout_handler', ['msg' => $msg]); 238 | } else { 239 | throw new ClusterTimeoutException("The RPC call has timed out, and no RPC server is currently reachable. please try again later."); 240 | } 241 | } 242 | 243 | // Return 244 | return $this->response; 245 | 246 | } 247 | 248 | /** 249 | * onResponse 250 | */ 251 | public function onResponse($response):void 252 | { 253 | 254 | // Check correlation id 255 | if ($response->get('correlation_id') != $this->correlation_id) { 256 | return; 257 | } 258 | 259 | // Get response 260 | $this->response = unserialize($response->body); 261 | } 262 | 263 | /** 264 | * Extract MessageRequestInterface object from body of message received by listener / consumer. 265 | */ 266 | public function extractMessage($msg):MessageRequestInterface 267 | { 268 | return unserialize($msg->body); 269 | } 270 | 271 | /** 272 | * Ack 273 | */ 274 | public function ack($msg):void 275 | { 276 | $msg->ack(); 277 | } 278 | 279 | /** 280 | * Nack 281 | */ 282 | public function nack($msg):void 283 | { 284 | $msg->nack(); 285 | } 286 | 287 | /** 288 | * Send reply for PRC call. 289 | */ 290 | public function reply($data, $msg):void 291 | { 292 | 293 | // Define new message 294 | $payload = new AMQPMessage( 295 | serialize($data), 296 | array('correlation_id' => $msg->get('correlation_id')) 297 | ); 298 | 299 | // Send reply 300 | $msg->delivery_info['channel']->basic_publish($payload, '', $msg->get('reply_to')); 301 | } 302 | 303 | /** 304 | * Fetch next message in queue. 305 | */ 306 | public function fetch (string $queue, bool $no_ack = false):?MessageRequestInterface 307 | { 308 | 309 | // Get message 310 | if (!$res = $this->channel->basic_get($queue, $no_ack)) { 311 | return null; 312 | } 313 | $this->ack($res); 314 | 315 | // Extract, and return 316 | return $this->extractMessage($res); 317 | } 318 | 319 | } 320 | 321 | -------------------------------------------------------------------------------- /src/Cluster.php: -------------------------------------------------------------------------------- 1 | ['type' => 'topic', 'durable' => false, 'no_ack' => true, 'auto_delete' => true], 28 | 'queue' => ['type' => 'topic', 'durable' => true, 'no_ack' => false, 'auto_delete' => false], 29 | 'broadcast' => ['type' => 'fanout', 'durable' => true, 'no_ack' => true, 'auto_delete' => false] 30 | ]; 31 | 32 | /** 33 | * Constructor 34 | */ 35 | public function __construct( 36 | public string $instance_name, 37 | public ?redis $redis = null, 38 | private string $router_file = '', 39 | private ?string $container_file = '' 40 | ) { 41 | 42 | // Setup container 43 | if ($this->container_file !== null) { 44 | $this->setupContainer(); 45 | } else { 46 | Di::set(__CLASS__, $this); 47 | $this->loadRoutes($this->router_file); 48 | } 49 | 50 | } 51 | 52 | /** 53 | * Setup container 54 | */ 55 | private function setupContainer() 56 | { 57 | 58 | // Get container file 59 | if ($this->container_file !== null && $this->container_file == '') { 60 | $this->container_file = __DIR__ . '/../config/container.php'; 61 | } 62 | 63 | // Check for redis and new container file 64 | if ($this->redis instanceof redis && $sha1_hash = $this->redis->get('cluster:container:sha1')) { 65 | 66 | // Check SHA1 hash of local file 67 | if ($sha1_hash != sha1_file($this->container_file)) { 68 | 69 | // Ensure container file is writeable 70 | if (!is_writeable($this->container_file)) { 71 | throw new ClusterFileNotWriteableException("New container file detected from redis, but container file is not writeable at: $this->container_file"); 72 | } 73 | 74 | // Save new container file 75 | $contents = $this->redis->get('cluster:container:items'); 76 | file_put_contents($this->container_file, unserialize($contents)); 77 | } 78 | } 79 | 80 | // Build container 81 | Di::buildContainer($this->container_file); 82 | 83 | // Set base container items 84 | Di::set(__CLASS__, $this); 85 | Di::set('cluster.container_file', $this->container_file); 86 | 87 | // Add redis to container 88 | $redis_val = $this->redis instanceof redis ? $this->redis : 'no_connect'; 89 | Di::set(redis::class, $redis_val); 90 | 91 | // Mark necessary items as services 92 | Di::markItemAsService(BrokerInterface::class); 93 | Di::markItemAsService(FeHandlerInterface::class); 94 | Di::markItemAsService(LoggerInterface::class); 95 | Di::markItemAsService(ReceiverInterface::class); 96 | 97 | // Instantiate logger 98 | $this->logger = Di::get(LoggerInterface::class); 99 | 100 | // Load routes 101 | $this->loadRoutes($this->router_file); 102 | } 103 | 104 | /** 105 | * set screen logging 106 | */ 107 | public function setScreenLogging(bool $logging):void 108 | { 109 | $this->screen_logging = $logging; 110 | } 111 | 112 | /** 113 | * Set message broker 114 | */ 115 | public function setBroker(BrokerInterface $broker) 116 | { 117 | Di::set(BrokerInterface::class, $broker); 118 | } 119 | 120 | /** 121 | * Add log 122 | */ 123 | public function addLog(string $message, string $level = 'info'):void 124 | { 125 | 126 | // Screen logging 127 | if ($this->screen_logging === true) { 128 | $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n"; 129 | fwrite(STDOUT, $line); 130 | } 131 | 132 | // Add log 133 | if ($this->logger !== null) { 134 | $this->logger->$level($message); 135 | } 136 | 137 | } 138 | 139 | } 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | cluster = new Cluster($instance_name); 27 | } else { 28 | $this->cluster = Di::get(Cluster::class); 29 | } 30 | 31 | // Get broker, and open connection 32 | $this->broker = Di::get(BrokerInterface::class); 33 | $this->broker->openChannel(); 34 | 35 | } 36 | 37 | /** 38 | * Dispatch a message 39 | */ 40 | public function dispatch(MessageRequestInterface $msg, string $msg_type = 'rpc', callable $fe_handler_callback = null):?MessageResponseInterface 41 | { 42 | 43 | // Set instance name on message, add log 44 | $msg->setInstanceName($this->cluster->instance_name); 45 | $msg->setType($msg_type); 46 | $this->cluster->addLog("Dispatching message on " . $msg->getType() . " to routing key: " . $msg->getRoutingKey()); 47 | 48 | // Send message 49 | $response = $this->broker->publish($msg); 50 | 51 | // Execute fe handler callback, if needed 52 | if ($msg->getType() == 'rpc' && $fe_handler_callback !== null) { 53 | $fe_handler = $response->getFeHandler(); 54 | call_user_func($fe_handler_callback, $fe_handler); 55 | } elseif ($msg->getType() == 'rpc' && Di::has('cluster.fe_handler_callback')) { 56 | $fe_handler = $response->getFeHandler(); 57 | Di::call('cluster.fe_handler_callback', ['handler' => $fe_handler]); 58 | } 59 | 60 | // Return 61 | return $response; 62 | 63 | } 64 | 65 | /** 66 | * Destructor 67 | */ 68 | public function __destruct() 69 | { 70 | 71 | // Close connection 72 | if (isset($this->broker) && $this->broker instanceof BrokerInterface) { 73 | $this->broker->closeChannel(); 74 | } 75 | 76 | } 77 | 78 | } 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/Exceptions/ClusterDuplicateRoutingAliasException.php: -------------------------------------------------------------------------------- 1 | actions[] = [$action, $data]; 26 | } 27 | 28 | /** 29 | * Purge all actions 30 | */ 31 | final public function purgeActions(string $action = ''):void 32 | { 33 | $this->actions = []; 34 | } 35 | 36 | /** 37 | * Get actions 38 | */ 39 | final public function getActions():array 40 | { 41 | return $this->actions; 42 | } 43 | 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/FeHandlers/Syrus.php: -------------------------------------------------------------------------------- 1 | vars[$key] = $value; 26 | } 27 | 28 | /** 29 | * Add block 30 | */ 31 | public function addBlock(string $name, array $values):void 32 | { 33 | 34 | if (!isset($this->blocked[$name])) { 35 | $this->blocked[$name] = []; 36 | } 37 | $this->blocks[$name][] = $values; 38 | 39 | } 40 | 41 | /** 42 | * Add callout 43 | */ 44 | public function addCallout(string $message, string $type = 'success'):void 45 | { 46 | $this->callouts[] = [$message, $type]; 47 | } 48 | 49 | /** 50 | * Set template file 51 | */ 52 | public function setTemplateFile(string $file, bool $is_locked = false) 53 | { 54 | $this->addAction('set_template_file', [$file, $is_locked]); 55 | } 56 | 57 | /** 58 | * Get vars 59 | */ 60 | public function getVars():array 61 | { 62 | return $this->vars; 63 | } 64 | 65 | /** 66 | * Get blocks 67 | */ 68 | public function getBlocks():array 69 | { 70 | return $this->blocks; 71 | } 72 | 73 | /** 74 | * Get callouts 75 | */ 76 | public function getCallouts():array 77 | { 78 | return $this->callouts; 79 | } 80 | 81 | 82 | 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/Fetcher.php: -------------------------------------------------------------------------------- 1 | cluster = $cluster; 30 | 31 | // Get routes table 32 | $this->map = $cluster->getRoutesMap($cluster->instance_name); 33 | if (count($this->map->getAllRoutes()) == 0) { 34 | throw new ClusterZeroRoutesException("There are no routes configured to listen to msg_type queue"); 35 | } 36 | 37 | // Declare exchange and queues 38 | $listener = new Listener(); 39 | $this->broker = $listener->declare(false); 40 | 41 | } 42 | 43 | /** 44 | * Fetch messages from queue. 45 | */ 46 | public function fetch(string $queue_name):?MessageRequestInterface 47 | { 48 | 49 | // Get queue alias 50 | if (!$queue_id = $this->map->checkAlias($queue_name)) { 51 | throw new ClusterOutOfBoundsException("Queue does not exist within router configuration, $queue_name"); 52 | } 53 | $queue_name = 'cluster.' . $queue_id; 54 | 55 | // Ensure queue is declared 56 | $defs = $this->cluster->exchange_defaults['queue']; 57 | $this->broker->declareQueue($queue_name, $defs['durable'], false, $defs['auto_delete']); 58 | 59 | // Fetch from queue 60 | $res = $this->broker->fetch($queue_name); 61 | 62 | // Return 63 | return $res; 64 | } 65 | 66 | /** 67 | * Destruct 68 | */ 69 | public function __destruct() 70 | { 71 | //$this->broker->closeChannel(); 72 | } 73 | 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/Interfaces/BrokerInterface.php: -------------------------------------------------------------------------------- 1 | declare($screen_logging, $max_msg); 43 | } else { 44 | $broker = $this->declare($screen_logging, $max_msg, $prepare_msg_handler); 45 | } 46 | $cluster = Di::get(Cluster::class); 47 | 48 | // Start listening 49 | $cluster->addLog("Listening on " . $cluster->instance_name . " for messages to " . implode(", ", array_keys($this->exchanges))) . " ..."; 50 | $broker->wait(); 51 | 52 | // Close connection 53 | $broker->closeChannel(); 54 | $cluster->addLog("Shutting down..."); 55 | } 56 | 57 | /** 58 | * Declare exchange and queues 59 | */ 60 | public function declare(bool $screen_logging = true, int $max_msg = 1, callable $prepare_msg_handler = null):BrokerInterface 61 | { 62 | 63 | // Get cluster 64 | $cluster = Di::get(Cluster::class); 65 | $cluster->setScreenLogging($screen_logging); 66 | $in_name = $cluster->instance_name; 67 | 68 | // Check for 'instances' configuration 69 | if (isset($cluster->instances[$in_name]) && isset($cluster->instances[$in_name]['max_msg'])) { 70 | $max_msg = (int) $cluster->instances[$in_name]['max_msg']; 71 | } 72 | 73 | // Get routes table 74 | $map = $cluster->getRoutesMap($cluster->instance_name); 75 | if (count($map->getAllRoutes()) == 0) { 76 | throw new ClusterZeroRoutesException("There are no routes configured to listen for instance $in_name"); 77 | } 78 | 79 | // Get receiver 80 | $receiver = Di::make(ReceiverInterface::class, ['map' => $map, 'prepare_msg_handler' => $prepare_msg_handler]); 81 | 82 | // Open connection 83 | $broker = Di::get(BrokerInterface::class); 84 | $broker->openChannel(); 85 | 86 | // Declare exchanges 87 | $this->exchanges = $map->getAllExchanges(); 88 | foreach ($this->exchanges as $msg_type => $queues) { 89 | 90 | // Set variables 91 | $ex_name = 'cluster.ex.' . $msg_type; 92 | $defs = $cluster->exchange_defaults[$msg_type]; 93 | 94 | // Declare exchange 95 | $broker->declareExchange($ex_name, $defs['type'], $defs['durable'], $defs['auto_delete']); 96 | $cluster->addLog("Declared exchange $msg_type"); 97 | 98 | // Declare queues and bindings 99 | foreach ($queues as $queue_id => $routes) { 100 | $name = 'cluster.' . $queue_id; 101 | $broker->declareQueue($name, $defs['durable'], false, $defs['auto_delete']); 102 | $cluster->addLog("Declared queue $queue_id for exchange $msg_type"); 103 | 104 | // Bind queue to routing keys 105 | foreach ($routes as $routing_key) { 106 | $broker->bindQueue($name, $ex_name, $routing_key); 107 | $cluster->addLog("Binding queue $queue_id to exchange $msg_type with routing key $routing_key"); 108 | } 109 | 110 | // Consume queue 111 | $broker->consume($name, $defs['no_ack'], false, [$receiver, 'receive'], $max_msg); 112 | } 113 | } 114 | Di::set(BrokerInterface::class, $broker); 115 | 116 | // Return 117 | return $broker; 118 | } 119 | 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/Message/MessageRequest.php: -------------------------------------------------------------------------------- 1 | params = $params; 33 | 34 | // Start request 35 | $this->request = [ 36 | 'mode' => php_sapi_name() == 'cli' ? 'cli' : 'http', 37 | 'host' => $_SERVER['HTTP_HOST'] ?? '', 38 | 'port' => $_SERVER['SERVER_PORT'] ?? 80, 39 | 'uri' => $_SERVER['REQUEST_URI'] ?? '', 40 | 'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET', 41 | 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '', 42 | 'post' => filter_input_array(INPUT_POST), 43 | 'get' => filter_input_array(INPUT_GET), 44 | 'cookie' => filter_input_array(INPUT_COOKIE), 45 | 'server' => filter_input_array(INPUT_SERVER), 46 | 'http_headers' => function_exists('getAllHeaders') ? getAllHeaders() : [] 47 | ]; 48 | 49 | // Add CLI args to request, if needed 50 | if ($this->request['mode'] == 'cli') { 51 | global $argv; 52 | $this->request['script_file'] = array_unshift($argv); 53 | $this->request['argv'] = $argv; 54 | } 55 | 56 | // Get caller function / class 57 | $trace = debug_backtrace(); 58 | $this->caller = array( 59 | 'file' => $trace[0]['file'] ?? '', 60 | 'line' => $trace[0]['line'] ?? 0, 61 | 'function' => $trace[1]['function'] ?? '', 62 | 'class' => $trace[1]['class'] ?? '' 63 | ); 64 | 65 | // Parse routing key 66 | if (!preg_match("/^(\w+?)\.(\w+?)\.(\w+)$/", strtolower($routing_key), $match)) { 67 | throw new ClusterInvalidRoutingKeyException("Invalid routing key, $routing_key. Must be formatted as x.y.z"); 68 | } 69 | 70 | } 71 | 72 | /** 73 | * Set instance name 74 | */ 75 | public function setInstanceName(string $name):void 76 | { 77 | $this->instance_name = $name; 78 | } 79 | 80 | /** 81 | * Set the message type. 82 | */ 83 | public function setType(string $type):void 84 | { 85 | Validator::validateMsgType($type); 86 | $this->type = $type; 87 | } 88 | 89 | /** 90 | * Set target 91 | */ 92 | public function setTarget(string $target):void 93 | { 94 | $this->target = $target; 95 | } 96 | 97 | /** 98 | * Get instance name 99 | */ 100 | public function getInstanceName():string { return $this->instance_name; } 101 | 102 | /** 103 | * Get the message type. 104 | */ 105 | public function getType():string { return $this->type; } 106 | 107 | /** 108 | * Get the routing key 109 | */ 110 | public function getRoutingKey():string { return $this->routing_key; } 111 | 112 | /** 113 | * Get the caller array. 114 | */ 115 | public function getCaller():array { return $this->caller; } 116 | 117 | /** 118 | * Get target 119 | */ 120 | public function getTarget():?string 121 | { 122 | return $this->target; 123 | } 124 | 125 | /** 126 | * Get request 127 | */ 128 | public function getRequest():array { return $this->request; } 129 | 130 | /** 131 | * Get the params of the request. 132 | */ 133 | public function getParams():mixed 134 | { 135 | return count($this->params) == 1 ? $this->params[0] : $this->params; 136 | } 137 | 138 | 139 | } 140 | 141 | -------------------------------------------------------------------------------- /src/Message/MessageResponse.php: -------------------------------------------------------------------------------- 1 | fe_handler = $fe_handler; 39 | $this->type = $msg->getType(); 40 | $this->instance_name = $msg->getInstanceName(); 41 | $this->routing_key = $msg->getRoutingKey(); 42 | $this->caller = $msg->getCaller(); 43 | $this->request = $msg->getRequest(); 44 | 45 | // Get params 46 | $this->params = $msg->getParams(); 47 | 48 | } 49 | 50 | /** 51 | * Set consumer name 52 | */ 53 | public function setConsumerName(string $name):void 54 | { 55 | $this->consumer_name = $name; 56 | } 57 | 58 | /** 59 | * Set status 60 | */ 61 | public function setStatus(int $status):void 62 | { 63 | $this->status = $status; 64 | } 65 | 66 | /** 67 | * Set front-end handler 68 | */ 69 | public function setFeHandler(FeHandlerInterface $fe_handler):void 70 | { 71 | $this->fe_handler = $fe_handler; 72 | } 73 | 74 | /** 75 | * Add response 76 | */ 77 | public function addResponse(string $alias, mixed $data):void 78 | { 79 | $this->response[$alias] = $data; 80 | } 81 | 82 | /** 83 | * Add called 84 | */ 85 | public function addCalled(string $alias, string $php_class):void 86 | { 87 | $this->called[$alias] = $php_class; 88 | } 89 | 90 | /** 91 | * Get consumer name 92 | */ 93 | public function getConsumerName():string { return $this->consumer_name; } 94 | 95 | /** 96 | * Get status 97 | */ 98 | public function getStatus():int { return $this->status; } 99 | 100 | /** 101 | * Get front-end handler 102 | */ 103 | public function getFeHandler():?FeHandlerInterface { return $this->fe_handler; } 104 | 105 | /** 106 | * Get response 107 | */ 108 | public function getResponse(string $alias = 'default'):mixed 109 | { 110 | return $this->response[$alias] ?? null; 111 | } 112 | 113 | /** 114 | * Get all responses 115 | */ 116 | public function getAllResponses():array { return $this->response; } 117 | 118 | /** 119 | * Get instance name 120 | */ 121 | public function getInstanceName():string { return $this->instance_name; } 122 | 123 | /** 124 | * Get called 125 | */ 126 | public function getCalled():array { return $this->called; } 127 | 128 | /** 129 | * Get the message type. 130 | */ 131 | public function getType():string { return $this->type; } 132 | 133 | /** 134 | * Get the routing key 135 | */ 136 | public function getRoutingKey():string { return $this->routing_key; } 137 | 138 | /** 139 | * Get the caller array. 140 | */ 141 | public function getCaller():array { return $this->caller; } 142 | 143 | /** 144 | * Get request 145 | */ 146 | public function getRequest():array { return $this->request; } 147 | 148 | /** 149 | * Get the params of the request. 150 | */ 151 | public function getParams():mixed 152 | { 153 | return count($this->params) == 1 ? $this->params[0] : $this->params; 154 | } 155 | 156 | 157 | } 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/Receiver.php: -------------------------------------------------------------------------------- 1 | extractMessage($payload); 42 | list($package, $class, $method) = explode('.', $msg->getRoutingKey()); 43 | 44 | // Invoke message preparation handler, if needed 45 | if ($this->prepare_msg_handler !== null) { 46 | call_user_func($this->prepare_msg_handler, $msg); 47 | } elseif (Di::has('cluster.prepare_msg_handler')) { 48 | Di::call('cluster.prepare_msg_handler', ['msg' => $msg]); 49 | } 50 | 51 | // Check for custom routing 52 | if (Di::has('cluster.custom_router')) { 53 | $php_classes = Di::call('cluster.custom_router', ['msg' => $msg]); 54 | } else { 55 | $php_classes = $this->map->queryRoutes($msg); 56 | } 57 | 58 | // Initialize response message, add log 59 | $response = new MessageResponse($msg); 60 | $fe_handler = Di::make(FeHandlerInterface::class); 61 | $cluster->addLog("Received message of " . $msg->getType() . " to routing key: " . $msg->getRoutingKey()); 62 | 63 | // GO through php classes 64 | foreach ($php_classes as $alias => $class_name) { 65 | 66 | // Check for auto-routing 67 | if (preg_match("/~(.+)~/", $class_name)) { 68 | $class_name = $this->getAutoRoutingClass($msg, $class_name); 69 | } 70 | 71 | // Execute method 72 | if (!$data = $this->executeMethod($class_name, $method, $msg, $fe_handler)) { 73 | continue; 74 | } 75 | $response->addResponse($alias, $data); 76 | 77 | // Add log 78 | $cluster->addLog("Executed method " . $class_name . "::" . $method . " for routing key " . $msg->getRoutingKey()); 79 | $response->addCalled($alias, $class_name); 80 | } 81 | 82 | // Send acknowledgement, if needed 83 | if ($msg->getType() != 'rpc') { 84 | $broker->ack($payload); 85 | } 86 | 87 | // Reply, if RPC call 88 | if ($msg->getType() == 'rpc') { 89 | $response->setFeHandler($fe_handler); 90 | return $broker->reply($response, $payload); 91 | } 92 | 93 | } 94 | 95 | /** 96 | * Execute php method of listener 97 | */ 98 | protected function executeMethod(string $class_name, string $method, MessageRequestInterface $msg, ?FeHandlerInterface $fe_handler = null):mixed 99 | { 100 | 101 | // Check if class exists 102 | if (class_exists($class_name)) { 103 | $consumer = Di::make($class_name); 104 | } else { 105 | return false; 106 | } 107 | 108 | // Execute method, and return 109 | if (!method_exists($consumer, $method)) { 110 | return null; 111 | } else { 112 | return $consumer->$method($msg, $fe_handler); 113 | } 114 | } 115 | 116 | /** 117 | * Get PHP class for auto-routing 118 | */ 119 | protected function getAutoRoutingClass(MessageRequestInterface $msg, string $php_class):string 120 | { 121 | 122 | // Initialize 123 | list($package, $module, $method) = explode('.', $msg->getRoutingKey()); 124 | 125 | // Initialize words 126 | $words = [ 127 | 'package' => $package, 128 | 'module' => $module, 129 | 'method' => $method, 130 | 'msg_type' => $msg->getType() 131 | ]; 132 | 133 | // Create merge vars 134 | $vars = []; 135 | foreach ($words as $key => $value) { 136 | $word = new UnicodeString($value); 137 | $vars['~' . $key . '~'] = $value; 138 | $vars['~' . $key . '.lower~'] = strtolower($value); 139 | $vars['~' . $key . '.camel~'] = $word->camel(); 140 | $vars['~' . $key . '.title~'] = $word->camel()->title(); 141 | } 142 | 143 | // Return 144 | return strtr($php_class, $vars); 145 | } 146 | 147 | } 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/Router/InstanceMap.php: -------------------------------------------------------------------------------- 1 | queues[$queue] = [ 38 | 'exchange' => $exchange, 39 | 'routes' => $routes 40 | ]; 41 | 42 | } 43 | 44 | /** 45 | * Add route 46 | */ 47 | public function addRoute(string $queue, string|int $id, array $route_vars):void 48 | { 49 | 50 | // Declare queue, if needed 51 | if (!isset($this->queues[$queue])) { 52 | $this->declareQueue($queue, $route_vars['type']); 53 | } 54 | 55 | // Add routing key to queue 56 | $key = $route_vars['routing_key']; 57 | $this->queues[$queue]['routes'][] = $key; 58 | 59 | // Add route 60 | if (!isset($this->routes[$key])) { 61 | $this->routes[$key] = []; 62 | } 63 | $this->routes[$key][] = $route_vars; 64 | 65 | // Define exchange type in queue 66 | if ($this->queues[$queue]['exchange'] != '' && $this->queues[$queue]['exchange'] != $route_vars['type']) { 67 | throw new ClusterExchangeTypeOverlayException("Unable to define queue '$queue' as exchange type '$route_vars[type]' as it is already defind as exchange type '" . $this->queues[$queue]['exchange'] . "'."); 68 | } 69 | $this->queues[$queue]['exchange'] = $route_vars['type']; 70 | 71 | // Add to alias map if msg_type is queue 72 | if ($route_vars['type'] == 'queue') { 73 | $this->alias_map[$route_vars['queue_name']] = $queue; 74 | } 75 | } 76 | 77 | /** 78 | * Get all routes 79 | */ 80 | public function getAllRoutes():array 81 | { 82 | return $this->routes; 83 | } 84 | 85 | /** 86 | * Get all exchanges 87 | */ 88 | public function getAllExchanges():array 89 | { 90 | 91 | // Go through queues 92 | $exchanges = []; 93 | foreach ($this->queues as $id => $vars) { 94 | $type = $vars['exchange']; 95 | 96 | if (!isset($exchanges[$type])) { 97 | $exchanges[$type] = []; 98 | } 99 | $exchanges[$type][$id] = $vars['routes']; 100 | } 101 | 102 | // Return 103 | return $exchanges; 104 | 105 | } 106 | 107 | /** 108 | * Query routes, return php classes to execute 109 | */ 110 | public function queryRoutes(MessageRequestInterface $msg):array 111 | { 112 | 113 | // Initialize 114 | $php_classes = []; 115 | $parts = explode('.', $msg->getRoutingKey()); 116 | $msg_type = $msg->getType(); 117 | 118 | // GO through all routes 119 | foreach ($this->routes as $key => $routes) { 120 | 121 | // Set variables 122 | $x = 0; 123 | $ok = true; 124 | $chk = explode('.', $key); 125 | 126 | // Check routing key 127 | foreach ($chk as $c) { 128 | if ($c == '*' || $c == $parts[$x++]) { 129 | continue; 130 | } 131 | $ok = false; 132 | } 133 | if ($ok === false) { continue; } 134 | 135 | // Go through routes, check params 136 | foreach ($routes as $id => $vars) { 137 | 138 | // Check msg type 139 | if ($msg_type != $vars['type']) { 140 | continue; 141 | } 142 | 143 | // Check params 144 | if (count($vars['params']) > 0 && !ParamChecker::check($vars['params'], $msg)) { 145 | continue; 146 | } 147 | $alias = $vars['alias']; 148 | 149 | // Add php class 150 | if (isset($php_classes[$alias]) && $key == '*.*.*') { 151 | continue; 152 | } elseif (isset($php_classes[$alias])) { 153 | throw new ClusterDuplicateRoutingAliasException("A duplicate routing alias of $alias already exists for the routing key $routing_key"); 154 | } 155 | $php_classes[$alias] = $vars['php_class']; 156 | } 157 | } 158 | 159 | // Return 160 | return $php_classes; 161 | 162 | } 163 | 164 | /** 165 | * Check alias 166 | */ 167 | public function checkAlias(string $queue_name):?string 168 | { 169 | return isset($this->alias_map[$queue_name]) ? $this->alias_map[$queue_name] : null; 170 | } 171 | } 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/Router/Loaders/RedisLoader.php: -------------------------------------------------------------------------------- 1 | set('cluster:container:items', serialize(file_get_contents($container_file))); 27 | $redis->set('cluster:container:sha1', sha1_file($container_file)); 28 | $redis->set('cluster:container:mtime', time()); 29 | 30 | // Save routes 31 | $redis->set('cluster:routes', serialize($routes)); 32 | } 33 | 34 | } 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Router/Loaders/YamlLoader.php: -------------------------------------------------------------------------------- 1 | load(file_get_contents($file)); 39 | } 40 | 41 | /** 42 | * Load Yaml from string 43 | */ 44 | public function load(string $yaml_code):void 45 | { 46 | 47 | // Initialize 48 | $cluster = Di::get(Cluster::class); 49 | 50 | // Load YAML code 51 | try { 52 | $yaml = Yaml::parse($yaml_code); 53 | } catch (ParseException $e) { 54 | throw new ParseException("Unable to parse YAML code for router. Message: " . $e->getMessage()); 55 | } 56 | 57 | // Check for ignore line 58 | if (isset($yaml['ignore']) && $yaml['ignore'] === true) { 59 | return; 60 | } 61 | 62 | // Validate yaml file 63 | Validator::validateYamlFile($yaml); 64 | 65 | // GO through all instances, if needed 66 | $instances = $yaml['instances'] ?? []; 67 | foreach ($instances as $name => $props) { 68 | $this->addInstance($name, $props); 69 | } 70 | 71 | // Go through all routes 72 | foreach ($yaml['routes'] as $name => $vars) { 73 | 74 | // Get instances 75 | $instances = $vars['instances'] ?? []; 76 | if (is_string($instances)) { 77 | $instances = [$instances]; 78 | } 79 | if (count($instances) == 0) { 80 | $instances[] = 'all'; 81 | } 82 | 83 | // Check for parameters 84 | $params = isset($vars['params']) && is_array($vars['params']) ? $vars['params'] : []; 85 | 86 | // Go through routing keys 87 | foreach ($vars['routing_keys'] as $routing_key => $php_classes) { 88 | 89 | // Check for string 90 | if (is_string($php_classes)) { 91 | $php_classes = ['default' => $php_classes]; 92 | } 93 | 94 | // Go through php classes 95 | foreach ($php_classes as $alias => $class_name) { 96 | $cluster->addRoute($routing_key, $class_name, $alias, $vars['type'], $name, $params, $instances, true); 97 | } 98 | } 99 | } 100 | 101 | } 102 | 103 | } 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/Router/Mapper.php: -------------------------------------------------------------------------------- 1 | []]; 19 | private array $queues = []; 20 | 21 | /** 22 | * Create instance map 23 | */ 24 | public function createInstanceMap(string $instance_name, array $routes):InstanceMap 25 | { 26 | 27 | // Map routes 28 | $this->mapRoutes($routes); 29 | 30 | // Instantiate new map 31 | $map = new InstanceMap($instance_name); 32 | 33 | // Get instance queues 34 | $in_queues = $this->instance_map[$instance_name] ?? $this->instance_map['all']; 35 | if (count($in_queues) == 0) { 36 | //throw new ClusterZeroRoutesException("No queues defined for the instance '$instance_name', and no routes to instance 'all' exist."); 37 | } 38 | 39 | // Go through queues 40 | foreach ($in_queues as $queue_id) { 41 | $map->declareQueue($queue_id); 42 | 43 | // Add routes as needed 44 | foreach ($this->queues[$queue_id] as $id) { 45 | $map->addRoute($queue_id, $id, $routes[$id]); 46 | } 47 | } 48 | 49 | // Return 50 | return $map; 51 | } 52 | 53 | /** 54 | * Map routes 55 | */ 56 | public function mapRoutes(array $routes):void 57 | { 58 | 59 | // Get routing IDs 60 | $routing_ids = $this->groupByRoutingId($routes); 61 | 62 | // Group into instances 63 | $instance_groups = $this->groupIdsByInstance($routing_ids); 64 | 65 | // Generate queues 66 | $this->queues = $this->generateQueues($instance_groups); 67 | 68 | } 69 | 70 | /** 71 | * Group routes by routing ID 72 | */ 73 | private function groupByRoutingId(array $routes):array 74 | { 75 | 76 | // Initialize 77 | list($routing_ids, $excludes, $all, $instance_ids) = array([], [], [], []); 78 | 79 | // GO through routes 80 | foreach ($routes as $id => $vars) { 81 | 82 | // Get routing id 83 | $routing_id = $vars['routing_id']; 84 | if (!isset($routing_ids[$routing_id])) { 85 | $routing_ids[$routing_id] = []; 86 | } 87 | 88 | // Add to routes map 89 | if (!isset($this->routes_map[$routing_id])) { 90 | $this->routes_map[$routing_id] = []; 91 | } 92 | $this->routes_map[$routing_id][] = $id; 93 | 94 | // Go through instances 95 | $added=0; 96 | foreach ($vars['instances'] as $instance) { 97 | 98 | // Check for exclude 99 | if (preg_match("/^~(.+)/", $instance, $match)) { 100 | $excludes[] = $match[1] . ':' . $routing_id; 101 | $instance_ids[$match[1]] = 1; 102 | continue; 103 | } elseif ($instance == 'all') { 104 | $all[] = $vars['type'] . '.' . $routing_id; 105 | } else { 106 | $instance_ids[$instance] = 1; 107 | } 108 | 109 | // Add to instances 110 | $routing_ids[$routing_id][] = $vars['type'] . '.' . $instance; 111 | $added++; 112 | } 113 | 114 | // Add to all, if no instances defined 115 | if ($added == 0) { 116 | $routing_ids[$routing_id][] = $vars['type'] . '.all'; 117 | $all[] = $vars['type'] . '.' . $routing_id; 118 | } 119 | } 120 | 121 | // GO through all 122 | foreach ($all as $def) { 123 | list($type, $id) = explode('.', $def, 2); 124 | 125 | 126 | foreach (array_keys($instance_ids) as $instance) { 127 | if (in_array($instance . ':' . $id, $excludes)) { continue; } 128 | if (in_array($type . '.' . $instance, $routing_ids[$id])) { continue; } 129 | $routing_ids[$id][] = $type . '.' . $instance; 130 | } 131 | } 132 | 133 | // Return 134 | return $routing_ids; 135 | } 136 | 137 | /** 138 | * Group routing IDs by instance 139 | */ 140 | private function groupIdsByInstance(array $routing_ids):array 141 | { 142 | 143 | // Map routes based on instance groupings 144 | $groupings = []; 145 | foreach ($routing_ids as $id => $instances) { 146 | 147 | // Get key 148 | sort($instances); 149 | $key = implode(' ', array_unique($instances)); 150 | 151 | // Ensure grouping key exists 152 | if (!isset($groupings[$key])) { 153 | $groupings[$key] = []; 154 | } 155 | $groupings[$key][] = $id; 156 | } 157 | 158 | // Return 159 | return $groupings; 160 | } 161 | 162 | /** 163 | * Generate queues from instance groupings 164 | */ 165 | private function generateQueues(array $instance_groups):array 166 | { 167 | 168 | // Generate queues 169 | $queues = []; 170 | foreach ($instance_groups as $in_key => $routing_ids) { 171 | 172 | // Add queue 173 | $queue_id = dechex(array_sum($routing_ids)); 174 | $queues[$queue_id] = []; 175 | 176 | // Add routing ids to queue 177 | foreach ($routing_ids as $id) { 178 | array_push($queues[$queue_id], ...$this->routes_map[$id]); 179 | } 180 | 181 | // Map to instances 182 | $instances = explode(' ', $in_key); 183 | foreach ($instances as $instance) { 184 | if (str_starts_with($instance, '~')) { continue; } 185 | $instance = preg_replace("/^(.+?)\./", "", $instance); 186 | 187 | if (!isset($this->instance_map[$instance])) { 188 | $this->instance_map[$instance] = []; 189 | } 190 | $this->instance_map[$instance][] = $queue_id; 191 | } 192 | } 193 | 194 | // Return 195 | return $queues; 196 | } 197 | 198 | } 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/Router/ParamChecker.php: -------------------------------------------------------------------------------- 1 | getRequest(); 26 | 27 | // Check params 28 | foreach ($params as $key => $condition) { 29 | 30 | // Get value being checked 31 | if (!$value = self::getValue($key, $request)) { 32 | $ok = false; 33 | break; 34 | } 35 | 36 | // Parse condition 37 | if (!preg_match("/^(..)(.*)$/", $condition, $match)) { 38 | throw new ClusterInvalidParamOperatorException("Invalid parameter condition, $condition"); 39 | } elseif (!in_array($match[1], ['==', '!=', '=~', '!~', '>=', '<='])) { 40 | throw new ClusterInvalidParamOperatorException("Invalid parameter operator, '$match[1]'. Supported operators are: ==, !=, =~, !~, >=, <="); 41 | } 42 | list($opr, $condition) = [$match[1], trim($match[2])]; 43 | 44 | // Check value 45 | if (!self::checkValue($value, $opr, $condition)) { 46 | $ok = false; 47 | break; 48 | } 49 | } 50 | 51 | // Return 52 | return $ok; 53 | } 54 | 55 | /** 56 | * Get value 57 | */ 58 | private static function getValue(string $key, array $request):?string 59 | { 60 | 61 | // Initialize 62 | $value = null; 63 | 64 | // Check for array element 65 | if (preg_match("/^(\w+?)\.(.+?)/", $key, $match) && 66 | isset($request[$match[1]]) && 67 | is_array($request[$match[1]]) && 68 | isset($request[$match[1]][$match[2]]) 69 | ) { 70 | $value = $request[$match[1]][$match[2]]; 71 | 72 | // Check singular element 73 | } elseif (isset($request[$key])) { 74 | $value = $request[$key]; 75 | } 76 | 77 | // Return 78 | return $value; 79 | } 80 | 81 | /** 82 | * Check value 83 | */ 84 | private static function checkValue(string $value, string $opr, string $condition):bool 85 | { 86 | 87 | // Check value depending on operator 88 | if ($opr == '==' && $value == $condition) { 89 | return true; 90 | } elseif ($opr == '!=' && $value != $condition) { 91 | return true; 92 | } elseif ($opr == '>=' && (int) $value >= (int) $condition) { 93 | return true; 94 | } elseif ($opr == '<=' && (int) $value <= (int) $condition) { 95 | return true; 96 | } elseif ($opr == '=~' && preg_match("/$condition/", $value)) { 97 | return true; 98 | } elseif ($opr == '!~' && !preg_match("/$condition/", $value)) { 99 | return true; 100 | } 101 | 102 | // Return 103 | return false; 104 | } 105 | 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/Router/Router.php: -------------------------------------------------------------------------------- 1 | get('cluster:routes')) { 31 | $this->routes = unserialize($routes_serialized); 32 | return; 33 | } 34 | 35 | // Load Yaml file 36 | $loader = new YamlLoader(); 37 | $loader->loadFile($yaml_file); 38 | } 39 | 40 | /** 41 | * Save to redis 42 | */ 43 | public function saveRedis(redis $redis, bool $restart_listeners = false):void 44 | { 45 | $client = new RedisLoader(); 46 | $client->save($redis, $this->routes); 47 | } 48 | 49 | /** 50 | * Add route 51 | */ 52 | public function addRoute( 53 | string $routing_key, 54 | string $php_class, 55 | string $alias = 'default', 56 | string $msg_type = 'rpc', 57 | string $queue_name = '', 58 | array $params = [], 59 | array $instances = [], 60 | bool $is_config = false 61 | ):string { 62 | 63 | // Validate routing key, php class and msg type 64 | Validator::validateRoutingKey($routing_key); 65 | Validator::validatePhpClassName($php_class); 66 | Validator::validateMsgType($msg_type); 67 | 68 | // Format routing key 69 | $routing_key = $this->formatRoutingKey($routing_key); 70 | 71 | // Check instances 72 | if (count($instances) == 0) { 73 | $instances[] = 'all'; 74 | } 75 | $routing_id = crc32($msg_type . ':' . $routing_key . ':' . implode(',', $instances) . ':' . serialize($params)); 76 | 77 | // Set route vars 78 | $vars = [ 79 | 'type' => $msg_type, 80 | 'routing_key' => $routing_key, 81 | 'alias' => $alias, 82 | 'php_class' => $php_class, 83 | 'instances' => $instances, 84 | 'params' => $params, 85 | 'routing_id' => $routing_id, 86 | 'queue_name' => $queue_name, 87 | 'is_config' => $is_config 88 | ]; 89 | 90 | // Add to routes 91 | $id = uniqid(); 92 | $this->routes[$id] = $vars; 93 | 94 | // Return 95 | return $id; 96 | } 97 | 98 | /** 99 | * Delete route 100 | */ 101 | public function deleteRoutes(string $routing_key = '', string $php_class = '', string $msg_type = '', string $instance = ''):int 102 | { 103 | 104 | // Go through routes 105 | $count = 0; 106 | foreach ($this->routes as $id => $vars) { 107 | 108 | // Skip, if needed 109 | if (($routing_key != '' && $routing_key != $vars['routing_key']) || 110 | ($php_class != '' && $php_class != $vars['php_class']) || 111 | ($msg_type != '' && $msg_type != $vars['type']) || 112 | ($instance != '' && !in_array($instance, $vars['instances'])) 113 | ) { continue; } 114 | 115 | // Delete route 116 | unset($this->routes[$id]); 117 | $count++; 118 | } 119 | 120 | // Return count 121 | return $count; 122 | } 123 | 124 | /** 125 | * Delete route by id 126 | */ 127 | public function deleteRouteId(string $id):bool 128 | { 129 | $ok = isset($this->routes[$id]) ? true : false; 130 | unset($this->routes[$id]); 131 | return $ok; 132 | } 133 | 134 | /** 135 | * Purge routes 136 | */ 137 | public function purgeRoutes():void 138 | { 139 | $this->routes = []; 140 | } 141 | 142 | /** 143 | * Create instance routes map 144 | */ 145 | public function GetRoutesMap(string $instance_name):?InstanceMap 146 | { 147 | 148 | // Create instance map 149 | $mapper = new Mapper(); 150 | $map = $mapper->createInstanceMap($instance_name, $this->routes); 151 | 152 | // Return 153 | return $map; 154 | } 155 | 156 | /** 157 | * Add instance 158 | */ 159 | public function addInstance(string $name, array $props):void 160 | { 161 | $this->instances[$name] = $props; 162 | } 163 | 164 | /** 165 | * Format routing key 166 | */ 167 | private function formatRoutingKey(string $routing_key):string 168 | { 169 | 170 | // Check for all 171 | if ($routing_key == 'all') { 172 | return '*.*.*'; 173 | } 174 | 175 | // Format, ensure three segments 176 | $parts = explode('.', $routing_key); 177 | if (count($parts) < 3) { 178 | do { 179 | $parts[] = '*'; 180 | } while (count($parts) < 3); 181 | } 182 | 183 | // Return 184 | return implode('.', $parts); 185 | } 186 | 187 | } 188 | 189 | -------------------------------------------------------------------------------- /src/Router/Validator.php: -------------------------------------------------------------------------------- 1 | $vars) { 29 | self::validateYamlRoute($name, $vars); 30 | } 31 | 32 | } 33 | 34 | /** 35 | * Validate YAML route 36 | */ 37 | public static function validateYamlRoute(string $name, mixed $vars):void 38 | { 39 | 40 | // Ensure vars is an array 41 | if (!is_array($vars)) { 42 | throw new ClusterYamlConfigException("The route '$name' within the YAML file is not an array."); 43 | } 44 | 45 | // Check required variables 46 | foreach (['type', 'routing_keys'] as $req) { 47 | if (!isset($vars[$req])) { 48 | throw new ClusterYamlConfigException("The route '$name' within Yaml config does not have a '$req' variable associated with it."); 49 | } 50 | } 51 | 52 | // Validate msg type 53 | self::validateMsgType($vars['type']); 54 | 55 | // Ensure routing keys exist 56 | if (!is_array($vars['routing_keys'])) { 57 | throw new ClusterYamlConfigException("Route '$name' does not contain an array of routing keys."); 58 | } 59 | 60 | // GO through routing keys 61 | foreach ($vars['routing_keys'] as $routing_key => $php_classes) { 62 | 63 | // Check routing key 64 | self::validateRoutingKey($routing_key); 65 | 66 | // Check for string 67 | if (is_string($php_classes)) { 68 | $php_classes = ['default' => $php_classes]; 69 | } 70 | 71 | // Go through php classes 72 | foreach ($php_classes as $alias => $class_name) { 73 | self::validatePhpClassName($class_name); 74 | } 75 | } 76 | 77 | } 78 | 79 | /** 80 | * Validate routing key 81 | */ 82 | public static function validateRoutingKey(string $routing_key, bool $use_strict = false):void 83 | { 84 | 85 | // Check parts 86 | $parts = explode('.', $routing_key); 87 | foreach ($parts as $part) { 88 | if (preg_match("/[\W\s]!\*/", $part)) { 89 | throw new ClusterInvalidRoutingKeyException("Routing key segments can not contain spaces or special characters, $routing_key"); 90 | } 91 | } 92 | 93 | // Check number of parts 94 | if (count($parts) > 3) { 95 | throw new ClusterInvalidRoutingKeyException("Routing keys can only have maximum of three segments, $routing_key"); 96 | } elseif ($use_strict === true && count($parts) != 3) { 97 | throw new ClusterInvalidRoutingKeyException("Routing keys must have exactly three segments, $routing_key"); 98 | } 99 | 100 | } 101 | 102 | /** 103 | * Validate php class name 104 | */ 105 | public static function validatePhpClassName(string $class_name):void 106 | { 107 | 108 | // Check for merge fields 109 | if (preg_match("/~(.+)~/", $class_name)) { 110 | return; 111 | } 112 | 113 | // Check 114 | if (class_exists($class_name) || file_exists($class_name)) { 115 | return; 116 | } 117 | throw new ClusterPhpClassNotExistsException("The php class / file does not exist, $class_name"); 118 | 119 | } 120 | 121 | /** 122 | * Validate msg type 123 | */ 124 | public static function validateMsgType(string $msg_type):void 125 | { 126 | if (!in_array($msg_type, ['rpc', 'queue', 'broadcast'])) { 127 | throw new ClusterInvalidArgumentException("Invalid msg_type '$msg_type'. Supported message types are: rpc, ack_only, queue, broadcast"); 128 | } 129 | } 130 | 131 | } 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/Sys/Sys.php: -------------------------------------------------------------------------------- 1 | addLog("Received shutdown command. Closing connection..."); 29 | $broker->closeChannel(); 30 | exit(0); 31 | } 32 | 33 | 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/router_test.php: -------------------------------------------------------------------------------- 1 | addRoute('users.profile', Apex\Cluster\Dispatcher::class); 25 | $upload_id = $c->addRoute('gallery.images', Apex\Cluster\Cluster::class, 'default', 'queue', 'images', [], ['app3', 'app4']); 26 | $broadcast_id = $c->addRoute('cluster', Apex\Cluster\Sys\Sys::class, 'cluster', 'broadcast'); 27 | $param_id = $c->addRoute('core.template.parse', Apex\Cluster\Listener::class, 'default', 'rpc', '', ['method' => '== POST', 'uri' => '=> login$'], ['app2']); 28 | $bitcoin_id = $c->addRoute('bitcoin.tx', Apex\Cluster\Router\Router::class); 29 | 30 | // Validate routes 31 | $this->assertCount(5, $c->routes); 32 | $this->validateRoute($c->routes[$profile_id], 'users.profile.*', Apex\Cluster\Dispatcher::class, 'rpc', 'all', 1); 33 | $this->validateRoute($c->routes[$upload_id], 'gallery.images.*', Apex\Cluster\Cluster::class, 'queue', 'app3', 2, 'images'); 34 | $this->validateRoute($c->routes[$broadcast_id], 'cluster.*.*', Apex\Cluster\Sys\Sys::class, 'broadcast', 'all', 1); 35 | $this->validateRoute($c->routes[$param_id], 'core.template.parse', Apex\Cluster\Listener::class, 'rpc', 'app2', 1); 36 | $this->assertCount(2, $c->routes[$param_id]['params']); 37 | 38 | // Delete route by id 39 | $this->assertArrayHasKey($bitcoin_id, $c->routes); 40 | $c->deleteRouteId($bitcoin_id); 41 | $this->assertArrayNotHasKey($bitcoin_id, $c->routes); 42 | $this->assertCount(4, $c->routes); 43 | } 44 | 45 | /** 46 | * Delete routes 47 | */ 48 | public function test_delete() 49 | { 50 | 51 | $c = Di::get(Cluster::class); 52 | $this->assertEquals(Cluster::class, $c::class); 53 | $this->assertCount(4, $c->routes); 54 | 55 | // Delete 56 | $c->deleteRoutes('', '', '', 'app4'); 57 | $this->assertCount(3, $c->routes); 58 | 59 | // Delete again 60 | $c->deleteRoutes('core.template.parse'); 61 | $this->assertCount(2, $c->routes); 62 | } 63 | 64 | 65 | /** 66 | * Purge routes 67 | */ 68 | public function test_purge() 69 | { 70 | 71 | // Get routes 72 | $c = Di::get(Cluster::class); 73 | $this->assertCount(2, $c->routes); 74 | 75 | // Purge routes 76 | $c->purgeRoutes(); 77 | $this->assertCount(0, $c->routes); 78 | 79 | } 80 | 81 | /** 82 | * Validate route 83 | */ 84 | private function validateRoute(array $r, string $routing_key, string $php_class = '', string $msg_type = '', string $instance = '', int $in_count = 0, string $queue_name = '') 85 | { 86 | // Check first route 87 | $this->assertIsArray($r); 88 | if ($routing_key != '') { 89 | $this->assertEquals($routing_key, $r['routing_key']); 90 | } 91 | 92 | if ($php_class != '') { 93 | $this->assertEquals($php_class, $r['php_class']); 94 | } 95 | 96 | if ($msg_type != '') { 97 | $this->assertEquals($msg_type, $r['type']); 98 | } 99 | 100 | if ($in_count > 0) { 101 | $this->assertCount($in_count, $r['instances']); 102 | } 103 | 104 | if ($instance != '') { 105 | $this->assertContains($instance, $r['instances']); 106 | } 107 | 108 | if ($queue_name != '') { 109 | $this->assertEquals($queue_name, $r['queue_name']); 110 | } 111 | 112 | } 113 | 114 | } 115 | 116 | 117 | -------------------------------------------------------------------------------- /tests/yaml_loader_test.php: -------------------------------------------------------------------------------- 1 | assertCount(0, $c->routes); 32 | $this->assertCount(0, $c->instances); 33 | 34 | } 35 | 36 | /** 37 | * Test full.yml example load 38 | */ 39 | public function test_full_load() 40 | { 41 | 42 | // Start cluster 43 | $c = new Cluster('app1', null, __DIR__ . '/../examples/yaml/full.yml'); 44 | $this->assertCount(7, $c->routes); 45 | 46 | } 47 | 48 | } 49 | 50 | 51 | --------------------------------------------------------------------------------