├── 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 |
--------------------------------------------------------------------------------