├── src
├── Exception
│ ├── RPCEndpointException.php
│ ├── ConfigFileNotFoundException.php
│ ├── MethodDoesNotExistException.php
│ ├── SerializerViolationException.php
│ ├── SingletonViolationException.php
│ ├── CallbackDoesNotExistException.php
│ ├── ConstantDoesNotExistException.php
│ ├── PropertyDoesNotExistException.php
│ ├── MagicMethodsExceptionsTrait.php
│ └── AmqpAgentException.php
├── Helper
│ ├── Event.php
│ ├── ClassProxy.php
│ ├── ArrayProxy.php
│ ├── EventTrait.php
│ ├── Example.php
│ ├── Singleton.php
│ ├── IDGenerator.php
│ ├── ArrayProxyTrait.php
│ ├── Logger.php
│ ├── ClassProxyTrait.php
│ ├── Utility.php
│ └── Serializer.php
├── Worker
│ ├── WorkerFacilitationInterface.php
│ ├── PublisherSingleton.php
│ ├── ConsumerSingleton.php
│ ├── WorkerMutationTrait.php
│ ├── PublisherInterface.php
│ ├── WorkerCommandTrait.php
│ ├── AbstractWorkerSingleton.php
│ ├── AbstractWorkerInterface.php
│ ├── ConsumerInterface.php
│ ├── AbstractWorker.php
│ └── Publisher.php
├── Config
│ ├── RPCEndpointParameters.php
│ ├── AbstractWorkerParameters.php
│ ├── PublisherParameters.php
│ ├── maks-amqp-agent-config.php
│ ├── AbstractParameters.php
│ ├── ConsumerParameters.php
│ └── AmqpAgentParameters.php
├── RPC
│ ├── AbstractEndpointInterface.php
│ ├── ClientEndpointInterface.php
│ ├── ServerEndpointInterface.php
│ ├── ServerEndpoint.php
│ ├── ClientEndpoint.php
│ └── AbstractEndpoint.php
├── Config.php
└── Client.php
├── composer.json
└── CHANGELOG.md
/src/Exception/RPCEndpointException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Endpoint violation exception.
18 | * @since 2.0.0
19 | */
20 | class RPCEndpointException extends AmqpAgentException
21 | {
22 | // RPCEndpointException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/ConfigFileNotFoundException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Config file not found exception.
18 | * @since 1.0.0
19 | */
20 | class ConfigFileNotFoundException extends AmqpAgentException
21 | {
22 | // ConfigFileNotFoundException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/MethodDoesNotExistException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Method does not exist exception.
18 | * @since 1.0.0
19 | */
20 | class MethodDoesNotExistException extends AmqpAgentException
21 | {
22 | // MethodDoesNotExistException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/SerializerViolationException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Serializer violation exception.
18 | * @since 1.0.0
19 | */
20 | class SerializerViolationException extends AmqpAgentException
21 | {
22 | // SerializerViolationException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/SingletonViolationException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Singleton violation exception.
18 | * @since 1.0.0
19 | */
20 | class SingletonViolationException extends AmqpAgentException
21 | {
22 | // SingletonViolationException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/CallbackDoesNotExistException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Callback does not exist exception.
18 | * @since 1.0.0
19 | */
20 | class CallbackDoesNotExistException extends AmqpAgentException
21 | {
22 | // CallbackDoesNotExistException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/ConstantDoesNotExistException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Constant does not exist exception.
18 | * @since 1.2.0
19 | */
20 | class ConstantDoesNotExistException extends AmqpAgentException
21 | {
22 | // ConstantDoesNotExistException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/PropertyDoesNotExistException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
15 |
16 | /**
17 | * Property does not exist exception.
18 | * @since 1.0.0
19 | */
20 | class PropertyDoesNotExistException extends AmqpAgentException
21 | {
22 | // PropertyDoesNotExistException
23 | }
24 |
--------------------------------------------------------------------------------
/src/Helper/Event.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use MAKS\AmqpAgent\Helper\EventTrait;
15 |
16 | /**
17 | * A simple class for handling events (dispatching and listening).
18 | *
19 | * Dispatch example:
20 | * ```
21 | * Event::dispatch('some.event.fired', [$arg1, $arg2]);
22 | * ```
23 | * Listen example:
24 | * ```
25 | * Event::listen('some.event.fired', function ($arg1, $arg2) {
26 | * mail('name@domain.tld', "The {$arg1} is ...!", "{$arg2} has been ....");
27 | * });
28 | * ```
29 | *
30 | * @since 2.0.0
31 | */
32 | class Event
33 | {
34 | use EventTrait {
35 | bind as public listen;
36 | trigger as public dispatch;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Worker/WorkerFacilitationInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | /**
15 | * An interface defining the simplest API to operate a worker.
16 | * @since 1.0.0
17 | */
18 | interface WorkerFacilitationInterface
19 | {
20 | /**
21 | * Executes all essential methods the worker needs before running its prime method (publish/consume).
22 | * @return self
23 | */
24 | public function prepare();
25 |
26 | /**
27 | * A function that takes the entire overhead of running a worker and wraps it in one single method with a possibility to change only the prime parameter of the worker (messages/callback).
28 | * @param mixed $parameter
29 | * @return void
30 | */
31 | public function work($parameter): void;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Config/RPCEndpointParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Config;
13 |
14 | use MAKS\AmqpAgent\Config\AbstractParameters;
15 | use MAKS\AmqpAgent\Config\AmqpAgentParameters;
16 |
17 | /**
18 | * A subset of AmqpAgentParameters class for RPC Endpoints classes.
19 | * @since 2.0.0
20 | */
21 | final class RPCEndpointParameters extends AbstractParameters
22 | {
23 | /**
24 | * The default connection options that the `ServerEndpoint` and `ClientEndpoint` should use when no overrides are provided.
25 | * @var array
26 | */
27 | public const RPC_CONNECTION_OPTIONS = AmqpAgentParameters::RPC_CONNECTION_OPTIONS;
28 |
29 | /**
30 | * The default queue name that the `ServerEndpoint` and `ClientEndpoint` should use when no overrides are provided.
31 | * @var array
32 | */
33 | public const RPC_QUEUE_NAME = AmqpAgentParameters::RPC_QUEUE_NAME;
34 | }
35 |
--------------------------------------------------------------------------------
/src/Helper/ClassProxy.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use MAKS\AmqpAgent\Helper\ClassProxyTrait;
15 |
16 | /**
17 | * A class containing methods for proxy methods calling, properties manipulation, and class utilities.
18 | *
19 | * Call example:
20 | * ```
21 | * ClassProxy::call($object, 'someMethod', $arguments);
22 | * ```
23 | * Get example:
24 | * ```
25 | * ClassProxy::get($object, 'someProperty');
26 | * ```
27 | * Set example:
28 | * ```
29 | * ClassProxy::set($object, 'someProperty', $newValue);
30 | * ```
31 | * Cast example:
32 | * ```
33 | * ClassProxy::cast($object, 'Namespace\SomeClass');
34 | * ```
35 | *
36 | * @since 2.0.0
37 | */
38 | class ClassProxy
39 | {
40 | use ClassProxyTrait {
41 | callMethod as call;
42 | setProperty as set;
43 | getProperty as get;
44 | castObjectToClass as cast;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/RPC/AbstractEndpointInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\RPC;
13 |
14 | use PhpAmqpLib\Connection\AMQPStreamConnection;
15 |
16 | /**
17 | * An interface defining the basic methods of an endpoint.
18 | * @since 2.0.0
19 | */
20 | interface AbstractEndpointInterface
21 | {
22 | /**
23 | * Opens a connection with RabbitMQ server.
24 | * @param array|null $connectionOptions [optional] The overrides for the default connection options of the RPC endpoint.
25 | * @return self
26 | */
27 | public function connect(?array $connectionOptions = []);
28 |
29 | /**
30 | * Closes the connection with RabbitMQ server.
31 | * @return void
32 | */
33 | public function disconnect(): void;
34 |
35 | /**
36 | * Returns whether the endpoint is connected or not.
37 | * @return bool
38 | */
39 | public function isConnected(): bool;
40 |
41 | /**
42 | * Returns the connection used by the endpoint.
43 | * @return AMQPStreamConnection
44 | */
45 | public function getConnection(): AMQPStreamConnection;
46 | }
47 |
--------------------------------------------------------------------------------
/src/Helper/ArrayProxy.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use MAKS\AmqpAgent\Helper\ArrayProxyTrait;
15 |
16 | /**
17 | * A class containing methods for for manipulating and working with arrays.
18 | *
19 | * Get example:
20 | * ```
21 | * ArrayProxy::get($array, 'someKey', 'this is a default/fallback value to use instead if not found');
22 | * ```
23 | * Set example:
24 | * ```
25 | * ArrayProxy::set($array, 'someKey', $newValue);
26 | * ```
27 | * Cast (array to string) example:
28 | * ```
29 | * ArrayProxy::arrayToString($array);
30 | * ```
31 | * Cast (array to object) example:
32 | * ```
33 | * ArrayProxy::arrayToObject($array);
34 | * ```
35 | * Cast (object to array) example:
36 | * ```
37 | * ArrayProxy::objectToArray($object);
38 | * ```
39 | *
40 | * @since 2.0.0
41 | */
42 | class ArrayProxy
43 | {
44 | use ArrayProxyTrait {
45 | getArrayValueByKey as get;
46 | setArrayValueByKey as set;
47 | castArrayToString as arrayToString;
48 | castArrayToObject as arrayToObject;
49 | castArrayToObject as objectToArray;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/RPC/ClientEndpointInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\RPC;
13 |
14 | use PhpAmqpLib\Message\AMQPMessage;
15 | use MAKS\AmqpAgent\RPC\AbstractEndpointInterface;
16 |
17 | /**
18 | * An interface defining the basic methods of a client.
19 | * @since 2.0.0
20 | */
21 | interface ClientEndpointInterface extends AbstractEndpointInterface
22 | {
23 | /**
24 | * Sends the passed request to the server using the passed queue.
25 | * @param string|AMQPMessage $request The request body or an `AMQPMessage` instance.
26 | * @param string|null $queueName [optional] The name of queue to send through.
27 | * @return string The response body.
28 | */
29 | public function request($request, ?string $queueName = null): string;
30 |
31 | /**
32 | * Sends the passed request to the server using the passed queue.
33 | * Alias for `self::request()`.
34 | * @param string|AMQPMessage $request The request body or an `AMQPMessage` instance.
35 | * @param string|null $queueName [optional] The name of queue to send through.
36 | * @return string The response body.
37 | */
38 | public function call($request, ?string $queueName = null): string;
39 | }
40 |
--------------------------------------------------------------------------------
/src/Config/AbstractWorkerParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Config;
13 |
14 | use MAKS\AmqpAgent\Config\AbstractParameters;
15 | use MAKS\AmqpAgent\Config\AmqpAgentParameters;
16 |
17 | /**
18 | * A subset of AmqpAgentParameters class for the AbstractWorker class.
19 | * @since 1.2.0
20 | */
21 | final class AbstractWorkerParameters extends AbstractParameters
22 | {
23 | /**
24 | * The default prefix for naming that is used when no name is provided.
25 | * @var array
26 | */
27 | public const PREFIX = AmqpAgentParameters::PREFIX;
28 |
29 | /**
30 | * The default connection options that the worker should use when no overrides are provided.
31 | * @var array
32 | */
33 | public const CONNECTION_OPTIONS = AmqpAgentParameters::CONNECTION_OPTIONS;
34 | /**
35 | * The default channel options that the worker should use when no overrides are provided.
36 | * @var array
37 | */
38 | public const CHANNEL_OPTIONS = AmqpAgentParameters::CHANNEL_OPTIONS;
39 |
40 | /**
41 | * The default queue options that the worker should use when no overrides are provided.
42 | * @var array
43 | */
44 | public const QUEUE_OPTIONS = AmqpAgentParameters::QUEUE_OPTIONS;
45 | }
46 |
--------------------------------------------------------------------------------
/src/Config/PublisherParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Config;
13 |
14 | use MAKS\AmqpAgent\Config\AbstractParameters;
15 | use MAKS\AmqpAgent\Config\AmqpAgentParameters;
16 |
17 | /**
18 | * A subset of AmqpAgentParameters class for the Publisher class.
19 | * @since 1.2.0
20 | */
21 | final class PublisherParameters extends AbstractParameters
22 | {
23 | /**
24 | * The default exchange options that the worker should use when no overrides are provided.
25 | * @var array
26 | */
27 | public const EXCHANGE_OPTIONS = AmqpAgentParameters::EXCHANGE_OPTIONS;
28 |
29 | /**
30 | * The default bind options that the worker should use when no overrides are provided.
31 | * @var array
32 | */
33 | public const BIND_OPTIONS = AmqpAgentParameters::BIND_OPTIONS;
34 |
35 | /**
36 | * The default message options that the worker should use when no overrides are provided.
37 | * @var array
38 | */
39 | public const MESSAGE_OPTIONS = AmqpAgentParameters::MESSAGE_OPTIONS;
40 |
41 | /**
42 | * The default publish options that the worker should use when no overrides are provided.
43 | * @var array
44 | */
45 | public const PUBLISH_OPTIONS = AmqpAgentParameters::PUBLISH_OPTIONS;
46 | }
47 |
--------------------------------------------------------------------------------
/src/RPC/ServerEndpointInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\RPC;
13 |
14 | use MAKS\AmqpAgent\RPC\AbstractEndpointInterface;
15 |
16 | /**
17 | * An interface defining the basic methods of a server.
18 | * @since 2.0.0
19 | */
20 | interface ServerEndpointInterface extends AbstractEndpointInterface
21 | {
22 | /**
23 | * Listens on requests coming via the passed queue and processes them with the passed callback.
24 | * Alias for `self::respond()`.
25 | * @param callable|null $callback [optional] The callback to process the request. This callback will be passed an `AMQPMessage` and must return a string.
26 | * @param string|null $queueName [optional] The name of the queue to listen on.
27 | * @return string The last processed request.
28 | */
29 | public function respond(?callable $callback = null, ?string $queueName = null): string;
30 |
31 | /**
32 | * Listens on requests coming via the passed queue and processes them with the passed callback.
33 | * Alias for `self::respond()`.
34 | * @param callable|null $callback [optional] The callback to process the request. This callback will be passed an `AMQPMessage` and must return a string.
35 | * @param string|null $queueName [optional] The name of the queue to listen on.
36 | * @return string The last processed request.
37 | */
38 | public function serve(?callable $callback = null, ?string $queueName = null): string;
39 | }
40 |
--------------------------------------------------------------------------------
/src/Helper/EventTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use Closure;
15 |
16 | /**
17 | * A trait containing events handling functions (adds events triggering and binding capabilities) to a class.
18 | * @since 2.0.0
19 | */
20 | trait EventTrait
21 | {
22 | /**
23 | * Here lives all bindings.
24 | * @var array
25 | */
26 | protected static $events = [];
27 |
28 | /**
29 | * Executes callbacks attached to the passed event with the passed arguments.
30 | * @param string $event Event name.
31 | * @param array $arguments [optional] Arguments array. Note that the arguments will be spread (`...$args`) on the callback.
32 | * @return void
33 | */
34 | protected static function trigger(string $event, array $arguments = []): void
35 | {
36 | if (isset(self::$events[$event]) && count(self::$events[$event])) {
37 | $callbacks = &self::$events[$event];
38 | foreach ($callbacks as $callback) {
39 | call_user_func_array($callback, array_values($arguments));
40 | }
41 | } else {
42 | self::$events[$event] = [];
43 | }
44 | }
45 |
46 | /**
47 | * Binds the passed function to the passed event.
48 | * @param string $event Event name.
49 | * @param Closure $function A closure to process the event.
50 | * @return void
51 | */
52 | protected static function bind(string $event, Closure $function): void
53 | {
54 | self::$events[$event][] = $function;
55 | }
56 |
57 | /**
58 | * Returns array of all registered events as an array `['event.name' => [$cb1, $cb2, ...]]`.
59 | * @return array
60 | */
61 | public static function getEvents(): array
62 | {
63 | return self::$events;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Config/maks-amqp-agent-config.php:
--------------------------------------------------------------------------------
1 | AmqpAgentParameters::PREFIX, // default
12 | // If you want to modify command* use ClassName::$variableName.
13 | 'commandPrefix' => AmqpAgentParameters::COMMAND_PREFIX,
14 | 'commandSyntax' => AmqpAgentParameters::COMMAND_SYNTAX,
15 | // End of static/constant class properties. specific to AMQP Agent.
16 |
17 | // AbstractWorker
18 | 'connectionOptions' => AmqpAgentParameters::CONNECTION_OPTIONS,
19 | 'channelOptions' => AmqpAgentParameters::CHANNEL_OPTIONS,
20 | 'queueOptions' => AmqpAgentParameters::QUEUE_OPTIONS,
21 |
22 | // Publisher
23 | 'exchangeOptions' => AmqpAgentParameters::EXCHANGE_OPTIONS,
24 | 'bindOptions' => AmqpAgentParameters::BIND_OPTIONS,
25 | 'messageOptions' => AmqpAgentParameters::MESSAGE_OPTIONS,
26 | 'publishOptions' => AmqpAgentParameters::PUBLISH_OPTIONS,
27 |
28 | // Consumer
29 | 'qosOptions' => AmqpAgentParameters::QOS_OPTIONS,
30 | 'waitOptions' => AmqpAgentParameters::WAIT_OPTIONS,
31 | 'consumeOptions' => AmqpAgentParameters::CONSUME_OPTIONS,
32 |
33 | // Start of constant class properties.
34 | // Only for reference, modifying them won't change anything
35 | 'ackOptions' => AmqpAgentParameters::ACK_OPTIONS,
36 | 'nackOptions' => AmqpAgentParameters::NACK_OPTIONS,
37 | 'getOptions' => AmqpAgentParameters::GET_OPTIONS,
38 | 'cancelOptions' => AmqpAgentParameters::CANCEL_OPTIONS,
39 | 'recoverOptions' => AmqpAgentParameters::RECOVER_OPTIONS,
40 | 'rejectOptions' => AmqpAgentParameters::REJECT_OPTIONS,
41 | // End of constant class properties.
42 |
43 | // RPC Endpoints
44 | 'rpcConnectionOptions' => AmqpAgentParameters::RPC_CONNECTION_OPTIONS,
45 | 'rpcQueueName' => AmqpAgentParameters::RPC_QUEUE_NAME,
46 | ];
47 |
--------------------------------------------------------------------------------
/src/Helper/Example.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use Exception;
15 | use PhpAmqpLib\Message\AMQPMessage;
16 | use MAKS\AmqpAgent\Helper\Logger;
17 | use MAKS\AmqpAgent\Helper\Serializer;
18 | use MAKS\AmqpAgent\Worker\Consumer;
19 |
20 | /**
21 | * An abstract class used as a default callback for the consumer.
22 | * @since 1.0.0
23 | */
24 | abstract class Example
25 | {
26 | /**
27 | * @var Serializer
28 | */
29 | private static $serializer;
30 |
31 | /**
32 | * Whether to log messages to a file or not.
33 | * @var bool
34 | */
35 | public static $logToFile = true;
36 |
37 |
38 | /**
39 | * Default AMQP Agent callback.
40 | * @param AMQPMessage $message
41 | * @return bool
42 | */
43 | public static function callback(AMQPMessage $message): bool
44 | {
45 | if (!isset(self::$serializer)) {
46 | self::$serializer = new Serializer();
47 | }
48 |
49 | try {
50 | $data = self::$serializer->unserialize($message->body, 'PHP', true);
51 | } catch (Exception $e) {
52 | // the strict value of the serializer is false here
53 | // because the data can also be plain-text
54 | $data = self::$serializer->unserialize($message->body, 'JSON', false);
55 | }
56 |
57 | Consumer::ack($message);
58 |
59 | if ($data && Consumer::isCommand($data)) {
60 | usleep(25000); // For acknowledgment to take effect.
61 | if (Consumer::hasCommand($data, 'close')) {
62 | Consumer::shutdown($message);
63 | }
64 | }
65 |
66 | if (static::$logToFile) {
67 | Logger::log($message->body, 'maks-amqp-agent-example-callback'); // @codeCoverageIgnore
68 | }
69 |
70 | return true;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Helper/Singleton.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use MAKS\AmqpAgent\Exception\SingletonViolationException;
15 |
16 | /**
17 | * An abstract class implementing the fundamental functionality of a singleton.
18 | * @since 1.0.0
19 | */
20 | abstract class Singleton
21 | {
22 | /**
23 | * Each sub-class of the Singleton stores its own instance here.
24 | * @var array
25 | */
26 | private static $instances = [];
27 |
28 |
29 | /**
30 | * Can't be private if we want to allow sub-classing.
31 | */
32 | protected function __construct()
33 | {
34 | //
35 | }
36 |
37 | /**
38 | * Cloning is not permitted for singletons.
39 | */
40 | public function __clone()
41 | {
42 | throw new SingletonViolationException("Bad call. Cannot clone a singleton!");
43 | }
44 |
45 | /**
46 | * Serialization is not permitted for singletons.
47 | */
48 | public function __sleep()
49 | {
50 | throw new SingletonViolationException("Bad call. Cannot serialize a singleton!");
51 | }
52 |
53 | /**
54 | * Unserialization is not permitted for singletons.
55 | */
56 | public function __wakeup()
57 | {
58 | throw new SingletonViolationException("Bad call. Cannot unserialize a singleton!");
59 | }
60 |
61 |
62 | /**
63 | * The method used to get the singleton's instance.
64 | * @return self
65 | */
66 | public static function getInstance()
67 | {
68 | $subclass = static::class;
69 | if (!isset(self::$instances[$subclass])) {
70 | self::$instances[$subclass] = new static();
71 | }
72 | return self::$instances[$subclass];
73 | }
74 |
75 |
76 | /**
77 | * Destroys the singleton's instance it was called on.
78 | * @param self &$object The instance it was called on.
79 | * @return void
80 | */
81 | public function destroyInstance(&$object)
82 | {
83 | if (is_subclass_of($object, __CLASS__)) {
84 | $object = null;
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Config/AbstractParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Config;
13 |
14 | use MAKS\AmqpAgent\Exception\ConstantDoesNotExistException;
15 |
16 | /**
17 | * An abstract class that exposes a simple API to work with parameters.
18 | * @since 1.2.0
19 | */
20 | abstract class AbstractParameters
21 | {
22 | /**
23 | * Patches the passed array with a class constant.
24 | * @param array $options The partial array.
25 | * @param string $const The constant name.
26 | * @param bool $values Whether to return values only or an associative array.
27 | * @return array The final patched array.
28 | * @throws ConstantDoesNotExistException
29 | */
30 | final public static function patch(array $options, string $const, bool $values = false): array
31 | {
32 | $final = null;
33 | $const = static::class . '::' . $const;
34 |
35 | if (defined($const)) {
36 | $const = constant($const);
37 |
38 | $final = is_array($const) ? self::patchWith($options, $const, $values) : $final;
39 | }
40 |
41 | if (null !== $final) {
42 | return $final;
43 | }
44 |
45 | throw new ConstantDoesNotExistException(
46 | sprintf(
47 | 'Could not find a constant with the name "%s", or the constant is not of type array!',
48 | $const
49 | )
50 | );
51 | }
52 |
53 | /**
54 | * Patches the passed array with another array.
55 | * @param array $partialArray The partial array.
56 | * @param array $fullArray The full array.
57 | * @param bool $values Whether to return values only or an associative array.
58 | * @return array The final patched array.
59 | */
60 | final public static function patchWith(array $partialArray, array $fullArray, bool $values = false): array
61 | {
62 | $final = (
63 | array_merge(
64 | $fullArray,
65 | array_intersect_key(
66 | $partialArray,
67 | $fullArray
68 | )
69 | )
70 | );
71 |
72 | return !$values ? $final : array_values($final);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Config/ConsumerParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Config;
13 |
14 | use MAKS\AmqpAgent\Config\AbstractParameters;
15 | use MAKS\AmqpAgent\Config\AmqpAgentParameters;
16 |
17 | /**
18 | * A subset of AmqpAgentParameters class for the Consumer class.
19 | * @since 1.2.0
20 | */
21 | final class ConsumerParameters extends AbstractParameters
22 | {
23 | /**
24 | * The default quality of service options that the worker should use when no overrides are provided.
25 | * @var array
26 | */
27 | public const QOS_OPTIONS = AmqpAgentParameters::QOS_OPTIONS;
28 |
29 | /**
30 | * The default wait options that the worker should use when no overrides are provided.
31 | * @var array
32 | */
33 | public const WAIT_OPTIONS = AmqpAgentParameters::WAIT_OPTIONS;
34 |
35 | /**
36 | * The default consume options that the worker should use when no overrides are provided.
37 | * @var array
38 | */
39 | public const CONSUME_OPTIONS = AmqpAgentParameters::CONSUME_OPTIONS;
40 |
41 | /**
42 | * The default acknowledgment options that the worker should use when no overrides are provided.
43 | * @var array
44 | */
45 | public const ACK_OPTIONS = AmqpAgentParameters::ACK_OPTIONS;
46 |
47 | /**
48 | * The default unacknowledgment options that the worker should use when no overrides are provided.
49 | * @var array
50 | */
51 | public const NACK_OPTIONS = AmqpAgentParameters::NACK_OPTIONS;
52 |
53 | /**
54 | * The default get options that the worker should use when no overrides are provided.
55 | * @var array
56 | */
57 | public const GET_OPTIONS = AmqpAgentParameters::GET_OPTIONS;
58 |
59 | /**
60 | * The default cancel options that the worker should use when no overrides are provided.
61 | * @var array
62 | */
63 | public const CANCEL_OPTIONS = AmqpAgentParameters::CANCEL_OPTIONS;
64 |
65 | /**
66 | * The default recover options that the worker should use when no overrides are provided.
67 | * @var array
68 | */
69 | public const RECOVER_OPTIONS = AmqpAgentParameters::RECOVER_OPTIONS;
70 |
71 | /**
72 | * The default reject options that the worker should use when no overrides are provided.
73 | * @var array
74 | */
75 | public const REJECT_OPTIONS = AmqpAgentParameters::REJECT_OPTIONS;
76 | }
77 |
--------------------------------------------------------------------------------
/src/Worker/PublisherSingleton.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Connection\AMQPStreamConnection;
15 | use PhpAmqpLib\Channel\AMQPChannel;
16 | use PhpAmqpLib\Message\AMQPMessage;
17 | use PhpAmqpLib\Wire\AMQPTable;
18 | use MAKS\AmqpAgent\Worker\Publisher;
19 | use MAKS\AmqpAgent\Worker\AbstractWorkerSingleton;
20 |
21 | /**
22 | * A singleton version of the Publisher class.
23 | * Static and constant properties are accessed via object operator (`->` not `::`).
24 | *
25 | * Example:
26 | * ```
27 | * $publisher = PublisherSingleton::getInstance();
28 | * ```
29 | *
30 | * @since 1.0.0
31 | * @api
32 | * @see \MAKS\AmqpAgent\Worker\Publisher for the full API.
33 | * @method self connect()
34 | * @method self disconnect()
35 | * @method self reconnect()
36 | * @method self queue(?array $parameters = null, ?AMQPChannel $_channel = null)
37 | * @method ?AMQPStreamConnection getConnection()
38 | * @method self setConnection(AMQPStreamConnection $connection)
39 | * @method ?AMQPChannel getChannel()
40 | * @method self setChannel(AMQPChannel $channel)
41 | * @method ?AMQPChannel getNewChannel(array $parameters = null, ?AMQPStreamConnection $_connection = null)
42 | * @method ?AMQPChannel getChannelById(array $parameters = null)
43 | * @method self exchange(?array $parameters = null, ?AMQPChannel $_channel = null)
44 | * @method self bind(?array $parameters = null, ?AMQPChannel $_channel = null)
45 | * @method AMQPMessage message(string $body, ?array $properties = null)
46 | * @method self publish($payload, ?array $parameters = null, ?AMQPChannel $_channel = null)
47 | * @method self publishBatch(array $messages, int $batchSize = 2500, ?array $parameters = null, ?AMQPChannel $_channel = null)
48 | * @method self prepare()
49 | * @method void work($messages)
50 | * @method static AMQPTable arguments(array $array)
51 | * @method static bool shutdown(...$object)
52 | * @method static array makeCommand(string $name, string $value, $parameters = null, string $argument = 'params')
53 | * @method static bool isCommand($data)
54 | * @method static bool hasCommand(array $data, string $name = null, ?string $value = null)
55 | * @method static mixed getCommand(array $data, string $key = 'params', ?string $sub = null)
56 | */
57 | final class PublisherSingleton extends AbstractWorkerSingleton
58 | {
59 | /**
60 | * Use PublisherSingleton::getInstance() instead.
61 | */
62 | public function __construct()
63 | {
64 | $this->worker = new Publisher();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Helper/IDGenerator.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | /**
15 | * A class containing functions for generating unique IDs and Tokens.
16 | * @since 2.0.0
17 | */
18 | final class IDGenerator
19 | {
20 | /**
21 | * Generates an md5 hash from microtime and uniqid.
22 | * @param string $entropy [optional] Additional entropy.
23 | * @return string
24 | */
25 | public static function generateHash(string $entropy = 'maks-amqp-agent-id'): string
26 | {
27 | $prefix = sprintf('-%s-[%d]-', $entropy, rand());
28 | $symbol = microtime(true) . uniqid($prefix, true);
29 |
30 | return md5($symbol);
31 | }
32 |
33 | /**
34 | * Generates a crypto safe unique token. Note that this function is pretty expensive.
35 | * @param int $length The length of the token. If the token is hashed this will not be the length of the returned string.
36 | * @param string|null $charset [optional] A string of characters to generate the token from. Defaults to alphanumeric.
37 | * @param string|null $hashing [optional] A name of hashing algorithm to hash the generated token with. Defaults to no hashing.
38 | * @return string
39 | */
40 | public static function generateToken(int $length = 32, ?string $charset = null, ?string $hashing = null): string
41 | {
42 | $token = '';
43 | $charset = $charset ?? (
44 | implode(range('A', 'Z')) .
45 | implode(range('a', 'z')) .
46 | implode(range(0, 9))
47 | );
48 | $max = strlen($charset);
49 |
50 | for ($i = 0; $i < $length; $i++) {
51 | $token .= $charset[
52 | self::generateCryptoSecureRandom(0, $max - 1)
53 | ];
54 | }
55 |
56 | return $hashing ? hash($hashing, $token) : $token;
57 | }
58 |
59 | /**
60 | * Generates a crypto secure random number.
61 | * @param int $min
62 | * @param int $max
63 | * @return int
64 | */
65 | protected static function generateCryptoSecureRandom(int $min, int $max): int
66 | {
67 | $range = $max - $min;
68 | if ($range < 1) {
69 | return $min;
70 | }
71 |
72 | $log = ceil(log($range, 2));
73 | $bytes = (int)(($log / 8) + 1); // length in bytes
74 | $bits = (int)($log + 1); // length in bits
75 | $filter = (int)((1 << $bits) - 1); // set all lower bits to 1
76 |
77 | do {
78 | $random = PHP_VERSION >= 7
79 | ? random_bytes($bytes)
80 | : openssl_random_pseudo_bytes($bytes);
81 | $random = hexdec(bin2hex($random));
82 | $random = $random & $filter; // discard irrelevant bits
83 | } while ($random > $range);
84 |
85 | return $min + $random;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Exception/MagicMethodsExceptionsTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use MAKS\AmqpAgent\Helper\ArrayProxy;
15 | use MAKS\AmqpAgent\Exception\PropertyDoesNotExistException;
16 | use MAKS\AmqpAgent\Exception\MethodDoesNotExistException;
17 |
18 | /**
19 | * A trait to throw exceptions on calls to magic methods.
20 | * @since 1.2.0
21 | */
22 | trait MagicMethodsExceptionsTrait
23 | {
24 | /**
25 | * Throws an exception when trying to get a class member via public property assignment notation.
26 | * @param string $property Property name.
27 | * @return void
28 | * @throws PropertyDoesNotExistException
29 | */
30 | public function __get(string $property)
31 | {
32 | throw new PropertyDoesNotExistException(
33 | sprintf(
34 | 'The requested property with the name "%s" does not exist!',
35 | $property
36 | )
37 | );
38 | }
39 |
40 | /**
41 | * Throws an exception when trying to set a class member via public property assignment notation.
42 | * @param string $property Property name.
43 | * @param array $value Property value.
44 | * @return void
45 | * @throws PropertyDoesNotExistException
46 | */
47 | public function __set(string $property, $value)
48 | {
49 | throw new PropertyDoesNotExistException(
50 | sprintf(
51 | 'A property with the name "%s" is immutable or does not exist!',
52 | $property
53 | )
54 | );
55 | }
56 |
57 | /**
58 | * Throws an exception for calls to undefined methods.
59 | * @param string $method Function name.
60 | * @param array $parameters Function arguments.
61 | * @return mixed
62 | * @throws MethodDoesNotExistException
63 | */
64 | public function __call(string $method, $parameters)
65 | {
66 | throw new MethodDoesNotExistException(
67 | sprintf(
68 | 'The called method "%s" with the parameter(s) "%s" does not exist!',
69 | $method,
70 | ArrayProxy::castArrayToString($parameters)
71 | )
72 | );
73 | }
74 |
75 | /**
76 | * Throws an exception for calls to undefined static methods.
77 | * @param string $method Function name.
78 | * @param array $parameters Function arguments.
79 | * @return mixed
80 | * @throws MethodDoesNotExistException
81 | */
82 | public static function __callStatic(string $method, $parameters)
83 | {
84 | throw new MethodDoesNotExistException(
85 | sprintf(
86 | 'The called static method "%s" with the parameter(s) "%s" does not exist',
87 | $method,
88 | ArrayProxy::castArrayToString($parameters)
89 | )
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Worker/ConsumerSingleton.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Connection\AMQPStreamConnection;
15 | use PhpAmqpLib\Channel\AMQPChannel;
16 | use PhpAmqpLib\Message\AMQPMessage;
17 | use PhpAmqpLib\Wire\AMQPTable;
18 | use MAKS\AmqpAgent\Worker\Consumer;
19 | use MAKS\AmqpAgent\Worker\AbstractWorkerSingleton;
20 |
21 | /**
22 | * A singleton version of the Consumer class.
23 | * Static and constant properties are accessed via object operator (`->` not `::`).
24 | *
25 | * Example:
26 | * ```
27 | * $consumer = ConsumerSingleton::getInstance();
28 | * ```
29 | *
30 | * @since 1.0.0
31 | * @api
32 | * @see \MAKS\AmqpAgent\Worker\Consumer for the full API.
33 | * @method self connect()
34 | * @method self disconnect()
35 | * @method self reconnect()
36 | * @method self queue(?array $parameters = null, ?AMQPChannel $_channel = null)
37 | * @method ?AMQPStreamConnection getConnection()
38 | * @method self setConnection(AMQPStreamConnection $connection)
39 | * @method ?AMQPChannel getChannel()
40 | * @method self setChannel(AMQPChannel $channel)
41 | * @method ?AMQPChannel getNewChannel(array $parameters = null, ?AMQPStreamConnection $_connection = null)
42 | * @method ?AMQPChannel getChannelById(array $parameters = null)
43 | * @method self qos(?array $parameters = null, ?AMQPChannel $_channel = null)
44 | * @method self consume($callback = null, ?array $variables = null, ?array $parameters = null, ?AMQPChannel $_channel = null)
45 | * @method bool isConsuming(?AMQPChannel $_channel = null)
46 | * @method self wait(?array $parameters = null, ?AMQPChannel $_channel = null)
47 | * @method self waitForAll(?array $parameters = null, ?AMQPStreamConnection $_connection = null)
48 | * @method self prepare()
49 | * @method void work($callback)
50 | * @method static AMQPTable arguments(array $array)
51 | * @method static bool shutdown(...$object)
52 | * @method static array makeCommand(string $name, string $value, $parameters = null, string $argument = 'params')
53 | * @method static bool isCommand($data)
54 | * @method static bool hasCommand(array $data, string $name = null, ?string $value = null)
55 | * @method static mixed getCommand(array $data, string $key = 'params', ?string $sub = null)
56 | * @method static void ack(AMQPMessage $_message, ?array $parameters = null)
57 | * @method static void nack(?AMQPChannel $_channel = null, AMQPMessage $_message, ?array $parameters = null)
58 | * @method static ?AMQPMessage get(AMQPChannel $_channel, ?array $parameters = null)
59 | * @method static mixed cancel(AMQPChannel $_channel, ?array $parameters = null)
60 | * @method static mixed recover(AMQPChannel $_channel, ?array $parameters = null)
61 | * @method static void reject(AMQPMessage $_message, ?array $parameters = null)
62 | */
63 | final class ConsumerSingleton extends AbstractWorkerSingleton
64 | {
65 | /**
66 | * Use ConsumerSingleton::getInstance() instead.
67 | */
68 | public function __construct()
69 | {
70 | $this->worker = new Consumer();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marwanalsoltany/amqp-agent",
3 | "type": "library",
4 | "license": "LGPL-2.1-or-later",
5 | "description": "An elegant wrapper around the famous php-amqplib for 90% use case.",
6 | "keywords": [
7 | "amqp",
8 | "amqp-agent",
9 | "php-amqplib",
10 | "rabbitmq",
11 | "message-broker",
12 | "php",
13 | "async",
14 | "queues"
15 | ],
16 | "authors": [
17 | {
18 | "name": "Marwan Al-Soltany",
19 | "email": "MarwanAlsoltany+gh@gmail.com",
20 | "homepage": "https://marwanalsoltany.github.io/",
21 | "role": "Developer"
22 | }
23 | ],
24 | "funding": [
25 | {
26 | "type": "ko-fi",
27 | "url": "https://ko-fi.com/marwanalsoltany"
28 | }
29 | ],
30 | "homepage": "https://github.com/MarwanAlsoltany/amqp-agent#readme",
31 | "support": {
32 | "docs": "https://marwanalsoltany.github.io/amqp-agent",
33 | "issues": "https://github.com/MarwanAlsoltany/amqp-agent/issues"
34 | },
35 | "require": {
36 | "php" : ">=7.1",
37 | "php-amqplib/php-amqplib": "^3.0"
38 | },
39 | "require-dev": {
40 | "squizlabs/php_codesniffer": "^3.5.5",
41 | "theseer/phpdox": "^0.12.0",
42 | "phpunit/phpunit": "^7.5.20",
43 | "phploc/phploc": "^4.0.1",
44 | "phpmd/phpmd": "^2.8.1"
45 | },
46 | "conflict": {
47 | "php": "7.4.0 - 7.4.1"
48 | },
49 | "suggest": {
50 | "symfony/console": "Symfony console component allows you to create CLI commands. Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.",
51 | "monolog/monolog": "Monolog sends your logs to files, sockets, inboxes, databases and various web services."
52 | },
53 | "autoload": {
54 | "psr-4": {
55 | "MAKS\\AmqpAgent\\": "src"
56 | }
57 | },
58 | "autoload-dev": {
59 | "psr-4": {
60 | "MAKS\\AmqpAgent\\Tests\\": "tests"
61 | }
62 | },
63 | "extra": {
64 | "branch-alias": {
65 | "dev-master": "2.2-dev"
66 | }
67 | },
68 | "scripts": {
69 | "sniff": "phpcs --report=xml --report-file=build/phpcs/index.xml src",
70 | "detect": "phpmd src xml naming,unusedcode --reportfile build/phpmd/index.xml --strict --ignore-violations-on-exit",
71 | "test": "phpunit",
72 | "measure": "phploc src --log-xml=build/phploc/index.xml",
73 | "document": "phpdox",
74 | "build": [
75 | "@sniff",
76 | "@detect",
77 | "@test",
78 | "@measure",
79 | "@document"
80 | ],
81 | "build-dev": [
82 | "composer run-script build --dev --verbose",
83 | "echo ! && echo ! Development build completed! && echo !"
84 | ],
85 | "build-prod": [
86 | "composer run-script build --quiet",
87 | "echo ! && echo ! Production build completed! && echo !"
88 | ]
89 | },
90 | "config": {
91 | "optimize-autoloader": true,
92 | "sort-packages": false,
93 | "process-timeout": 0
94 | },
95 | "minimum-stability": "dev",
96 | "prefer-stable": true
97 | }
98 |
--------------------------------------------------------------------------------
/src/Worker/WorkerMutationTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | /**
15 | * A trait containing the implementation of members mutation functions.
16 | * @since 1.0.0
17 | */
18 | trait WorkerMutationTrait
19 | {
20 | /**
21 | * The last mutation happened to a class member (for debugging purposes).
22 | * @var array
23 | */
24 | protected $mutation = [];
25 |
26 | /**
27 | * Mutates a subset of an array (class property) and returns the replaced subset.
28 | * @param string $member The name of the property.
29 | * @param array $overrides An associative array of the overrides.
30 | * @return array
31 | */
32 | protected function mutateClassMember(string $member, array $overrides): array
33 | {
34 | return $this->mutateClass($member, null, $overrides);
35 | }
36 |
37 | /**
38 | * Mutates a subset of an array inside a class property (nested array inside a property) and returns the replaced subset.
39 | * @param string $member The name of the property.
40 | * @param string $sub The key which under the array stored.
41 | * @param array $overrides An associative array of the overrides.
42 | * @return array
43 | */
44 | protected function mutateClassSubMember(string $member, string $sub, array $overrides): array
45 | {
46 | return $this->mutateClass($member, $sub, $overrides);
47 | }
48 |
49 | /**
50 | * Mutates a class property nested or not and returns the replaced subset.
51 | * @param string $member The name of the property.
52 | * @param string|null $sub [optional] The key which under the array stored.
53 | * @param array $overrides An associative array of the overrides.
54 | * @return array
55 | */
56 | private function mutateClass(string $member, ?string $sub, array $overrides): array
57 | {
58 | $changes = [];
59 | $signature = '@UNKNOWN[%s]';
60 |
61 | foreach ($overrides as $key => $value) {
62 | if ($sub) {
63 | if (isset($this->{$member}[$sub][$key])) {
64 | $changes[$key] = $this->{$member}[$sub][$key];
65 | } else {
66 | $changes[$key] = sprintf($signature, $key);
67 | }
68 | if ($value === sprintf($signature, $key)) {
69 | unset($this->{$member}[$sub][$key]);
70 | } else {
71 | $this->{$member}[$sub][$key] = $value;
72 | }
73 | } else {
74 | if (isset($this->{$member}[$key])) {
75 | $changes[$key] = $this->{$member}[$key];
76 | } else {
77 | $changes[$key] = sprintf($signature, $key);
78 | }
79 | if ($value === sprintf($signature, $key)) {
80 | unset($this->{$member}[$key]);
81 | } else {
82 | $this->{$member}[$key] = $value;
83 | }
84 | }
85 | }
86 |
87 | $this->mutation = [
88 | 'member' => $member,
89 | 'old' => $changes,
90 | 'new' => $overrides,
91 | ];
92 |
93 | return $changes;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Worker/PublisherInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Channel\AMQPChannel;
15 | use PhpAmqpLib\Message\AMQPMessage;
16 | use MAKS\AmqpAgent\Worker\AbstractWorkerInterface;
17 |
18 | /**
19 | * An interface defining the basic methods of a publisher.
20 | * @since 1.0.0
21 | */
22 | interface PublisherInterface extends AbstractWorkerInterface
23 | {
24 | /**
25 | * Declares an exchange on the default channel of the worker's connection to RabbitMQ server.
26 | * @param array|null $parameters [optional] The overrides for the default exchange options of the worker.
27 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
28 | * @return self
29 | */
30 | public function exchange(?array $parameters = null, ?AMQPChannel $_channel = null);
31 |
32 | /**
33 | * Binds the default queue to the default exchange on the default channel of the worker's connection to RabbitMQ server.
34 | * @param array|null $parameters [optional] The overrides for the default bind options of the worker.
35 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
36 | * @return self
37 | */
38 | public function bind(?array $parameters = null, ?AMQPChannel $_channel = null);
39 |
40 | /**
41 | * Returns an AMQPMessage object.
42 | * @param string $body The body of the message.
43 | * @param array|null $properties [optional] The overrides for the default properties of the default message options of the worker.
44 | * @return AMQPMessage
45 | */
46 | public function message(string $body, ?array $properties = null): AMQPMessage;
47 |
48 | /**
49 | * Publishes a message to the default exchange on the default channel of the worker's connection to RabbitMQ server.
50 | * @param string|array|AMQPMessage $payload A string of the body of the message or an array of body and properties for the message or a AMQPMessage object.
51 | * @param array|null $parameters [optional] The overrides for the default publish options of the worker.
52 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
53 | * @return self
54 | */
55 | public function publish($payload, ?array $parameters = null, ?AMQPChannel $_channel = null);
56 |
57 | /**
58 | * Publishes a batch of messages to the default exchange on the default channel of the worker's connection to RabbitMQ server.
59 | * @param string[]|array[]|AMQPMessage[] $messages An array of bodies of the messages or an array of arrays of body and properties for the messages or an array of message objects.
60 | * @param int $batchSize [optional] The number of messages that should be published per batch.
61 | * @param array|null $parameters [optional] The overrides for the default exchange options of the worker.
62 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
63 | * @return self
64 | */
65 | public function publishBatch(array $messages, int $batchSize = 1000, ?array $parameters = null, ?AMQPChannel $_channel = null);
66 | }
67 |
--------------------------------------------------------------------------------
/src/Exception/AmqpAgentException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Exception;
13 |
14 | use Exception as CoreException;
15 | use MAKS\AmqpAgent\Helper\Utility;
16 |
17 | /**
18 | * AMQP Agent base exception class.
19 | * @since 1.0.0
20 | */
21 | class AmqpAgentException extends CoreException
22 | {
23 | /**
24 | * Redefine the exception so message is not an optional parameter.
25 | * @param string $message
26 | * @param int $code
27 | * @param CoreException|null $previous
28 | */
29 | public function __construct(string $message, int $code = 0, CoreException $previous = null)
30 | {
31 | parent::__construct($message, $code, $previous);
32 | }
33 |
34 | /**
35 | * String representation of the object.
36 | * @return string
37 | */
38 | public function __toString()
39 | {
40 | return static::class . ": [{$this->code}]: {$this->message}\n{$this->getTraceAsString()}\n";
41 | }
42 |
43 | /**
44 | * Rethrows an exception with an additional message.
45 | * @param CoreException $exception The exception to rethrow.
46 | * @param string|null $message [optional] An additional message to add to the wrapping exception before the message of the passed exception.
47 | * @param string|bool $wrap [optional] Whether to throw the exception using the passed class (FQN), in the same exception type (true), or wrap it with the class this method was called on (false). Any other value will be translated to false. Defaults to true.
48 | * @return void
49 | * @throws CoreException
50 | */
51 | public static function rethrow(CoreException $exception, ?string $message = null, $wrap = true): void
52 | {
53 | if (null === $message) {
54 | $trace = Utility::backtrace(['file', 'line', 'class', 'function']);
55 | $prefix = (isset($trace['class']) ? "{$trace['class']}::" : "{$trace['file']}({$trace['line']}): ");
56 | $suffix = "{$trace['function']}() failed!";
57 | $message = 'Rethrown Exception: ' . $prefix . $suffix . ' ';
58 | } else {
59 | $message = strlen($message) ? $message . ' ' : $message;
60 | }
61 |
62 | $error = is_string($wrap)
63 | ? (
64 | class_exists($wrap) && is_subclass_of($wrap, 'Exception')
65 | ? $wrap
66 | : static::class
67 | )
68 | : (
69 | boolval($wrap)
70 | ? get_class($exception)
71 | : static::class
72 | );
73 |
74 | throw new $error($message . (string)$exception->getMessage(), (int)$exception->getCode(), $exception);
75 | }
76 |
77 | /**
78 | * Rethrows an exception with an additional message.
79 | * @deprecated 1.2.0 Use `self::rethrow()` instead.
80 | * @param CoreException $exception The exception to rethrow.
81 | * @param string|null $message [optional] An additional message to add to the wrapping exception before the message of the passed exception.
82 | * @param string|bool $wrap [optional] Whether to throw the exception using the passed class (FQN), in the same exception type (true), or wrap it with the class this method was called on (false). Any other value will be translated to false.
83 | * @return void
84 | * @throws CoreException
85 | */
86 | public static function rethrowException(CoreException $exception, ?string $message = null, $wrap = true): void
87 | {
88 | static::rethrow($exception, $message, $wrap);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Worker/WorkerCommandTrait.php:
--------------------------------------------------------------------------------
1 |
6 | * @copyright Marwan Al-Soltany 2020
7 | * For the full copyright and license information, please view
8 | * the LICENSE file that was distributed with this source code.
9 | */
10 |
11 | declare(strict_types=1);
12 |
13 | namespace MAKS\AmqpAgent\Worker;
14 |
15 | use MAKS\AmqpAgent\Config\AmqpAgentParameters;
16 |
17 | /**
18 | * A trait containing the implementation of the workers command interface/functions.
19 | * @since 1.0.0
20 | */
21 | trait WorkerCommandTrait
22 | {
23 | /**
24 | * The prefix that should be used to define an array as a command.
25 | * @var string
26 | */
27 | public static $commandPrefix = AmqpAgentParameters::COMMAND_PREFIX;
28 |
29 | /**
30 | * The recommended way of defining a command array.
31 | * @var array
32 | */
33 | public static $commandSyntax = AmqpAgentParameters::COMMAND_SYNTAX;
34 |
35 |
36 | /**
37 | * Constructs a command from passed data to a command array following the recommended pattern.
38 | * @param string $name The name of the command.
39 | * @param string $value The value of the command.
40 | * @param mixed $parameters [optional] Additional parameters to add to the command.
41 | * @param string $argument [optional] The key to use to store the parameters under.
42 | * @return array
43 | */
44 | public static function makeCommand(string $name, string $value, $parameters = null, string $argument = 'params'): array
45 | {
46 | $prefix = static::$commandPrefix;
47 | $result = [
48 | $prefix => []
49 | ];
50 |
51 | if ($name && $value) {
52 | $result[$prefix] = [
53 | $name => $value
54 | ];
55 | if ($parameters) {
56 | $result[$prefix][$argument] = $parameters;
57 | }
58 | }
59 |
60 | return $result;
61 | }
62 |
63 | /**
64 | * Checks whether an array is a command following the recommended pattern.
65 | * @param mixed $data The data that should be checked.
66 | * @return bool
67 | */
68 | public static function isCommand($data): bool
69 | {
70 | $prefix = static::$commandPrefix;
71 |
72 | $result = $data && is_array($data) && array_key_exists($prefix, $data);
73 |
74 | return $result;
75 | }
76 |
77 | /**
78 | * Checks whether a specific command (command name) exists in the command array.
79 | * @param array $data The array that should be checked.
80 | * @param string|null $name The name of the command.
81 | * @param string|null $value The value of the command.
82 | * @return bool
83 | */
84 | public static function hasCommand(array $data, string $name = null, ?string $value = null): bool
85 | {
86 | $prefix = static::$commandPrefix;
87 | $result = static::isCommand($data);
88 |
89 | $result = ($result && $name && array_key_exists($name, $data[$prefix]))
90 | ? true
91 | : $result;
92 |
93 | if ($result && $name && $value) {
94 | $result = isset($data[$prefix][$name]) && $data[$prefix][$name] === $value;
95 | }
96 |
97 | return $result;
98 | }
99 |
100 | /**
101 | * Returns the content of a specific key in the command array, used for example to get the additional parameters.
102 | * @param array $data The array that should be checked.
103 | * @param string $key [optional] The array key name.
104 | * @param string|null $sub [optional] The array nested array key name.
105 | * @return mixed
106 | */
107 | public static function getCommand(array $data, string $key = 'params', ?string $sub = null)
108 | {
109 | $prefix = static::$commandPrefix;
110 | $result = static::isCommand($data);
111 |
112 | if ($result) {
113 | $result = $data[$prefix];
114 | }
115 | if ($result && $key) {
116 | $result = array_key_exists($key, $data[$prefix])
117 | ? $data[$prefix][$key]
118 | : null;
119 | }
120 | if ($result && $sub) {
121 | $result = array_key_exists($sub, $data[$prefix][$key])
122 | ? $data[$prefix][$key][$sub]
123 | : null;
124 | }
125 |
126 | return $result;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Worker/AbstractWorkerSingleton.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use ReflectionClass;
15 | use MAKS\AmqpAgent\Helper\Singleton;
16 | use MAKS\AmqpAgent\Worker\AbstractWorker;
17 |
18 | /**
19 | * An abstract decorator class implementing mapping functions (proxy functions) to turn a normal worker into a singleton.
20 | * @since 1.0.0
21 | */
22 | abstract class AbstractWorkerSingleton extends Singleton
23 | {
24 | /**
25 | * The full qualified name of the instantiated class.
26 | * @var string
27 | */
28 | protected static $class;
29 |
30 | /**
31 | * The instance of the worker class (a class that extends AbstractWorker).
32 | * Sub-classes of this class should instantiate a worker and set it
33 | * to the protected $worker property in their __construct() method.
34 | * @var AbstractWorker
35 | */
36 | protected $worker;
37 |
38 |
39 | /**
40 | * Returns an instance of the class this method was called on.
41 | * @param array ...$arguments The same arguments of the normal worker.
42 | * @return self
43 | */
44 | public static function getInstance()
45 | {
46 | $worker = parent::getInstance();
47 | $workerReference = $worker->worker;
48 |
49 | static::$class = get_class($workerReference);
50 |
51 | $arguments = func_get_args();
52 | $argsCount = func_num_args();
53 |
54 | if ($argsCount > 0) {
55 | $reflection = new ReflectionClass($workerReference);
56 | $properties = $reflection->getConstructor()->getParameters();
57 |
58 | $index = 0;
59 | foreach ($properties as $property) {
60 | $member = $property->getName();
61 | $workerReference->{$member} = $arguments[$index];
62 | $index++;
63 | if ($index === $argsCount) {
64 | break;
65 | }
66 | }
67 | }
68 |
69 | return $worker;
70 | }
71 |
72 |
73 | /**
74 | * Gets a class member via public property access notation.
75 | * @param string $member Property name.
76 | * @return mixed
77 | */
78 | public function __get(string $member)
79 | {
80 | if (defined(static::$class . '::' . $member)) {
81 | return constant(static::$class . '::' . $member);
82 | } elseif (isset(static::$class::$$member)) {
83 | return static::$class::$$member;
84 | }
85 |
86 | return $this->worker->$member;
87 | }
88 |
89 | /**
90 | * Sets a class member via public property assignment notation.
91 | * @param string $member Property name.
92 | * @param mixed $value Override for object property or a static property.
93 | * @return void
94 | */
95 | public function __set(string $member, $value)
96 | {
97 | if (isset(static::$class::$$member)) {
98 | static::$class::$$member = $value;
99 | return;
100 | }
101 |
102 | $this->worker->{$member} = $value;
103 | }
104 |
105 | /**
106 | * Calls a method on a class that extend AbstractWorker and throws an exception for calls to undefined methods.
107 | * @param string $method Function name.
108 | * @param array $arguments Function arguments.
109 | * @return mixed
110 | */
111 | public function __call(string $method, array $arguments)
112 | {
113 | $function = [$this->worker, $method];
114 | $return = call_user_func_array($function, $arguments);
115 |
116 | // check to return the right object to allow for trouble-free chaining.
117 | if ($return instanceof $this->worker) {
118 | return $this;
119 | }
120 |
121 | return $return;
122 | }
123 |
124 | /**
125 | * Calls a method on a class that extend AbstractWorker and throws an exception for calls to undefined static methods.
126 | * @param string $method Function name.
127 | * @param array $arguments Function arguments.
128 | * @return mixed
129 | */
130 | public static function __callStatic(string $method, array $arguments)
131 | {
132 | $function = [static::$class, $method];
133 | $return = forward_static_call_array($function, $arguments);
134 |
135 | return $return;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/Worker/AbstractWorkerInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Connection\AMQPStreamConnection;
15 | use PhpAmqpLib\Channel\AMQPChannel;
16 | use PhpAmqpLib\Message\AMQPMessage;
17 | use PhpAmqpLib\Wire\AMQPTable;
18 |
19 | /**
20 | * An interface defining the basic methods of a worker.
21 | * @since 1.0.0
22 | */
23 | interface AbstractWorkerInterface
24 | {
25 | /**
26 | * Closes the connection or the channel or both with RabbitMQ server.
27 | * @param AMQPStreamConnection|AMQPChannel|AMQPMessage ...$object The object that should be used to close the channel or the connection.
28 | * @return bool True on success.
29 | */
30 | public static function shutdown(...$object): bool;
31 |
32 | /**
33 | * Returns an AMQPTable object.
34 | * @param array $array An array of the option wished to be turn into the an arguments object.
35 | * @return AMQPTable
36 | */
37 | public static function arguments(array $array): AMQPTable;
38 |
39 |
40 | /**
41 | * Establishes a connection with RabbitMQ server and opens a channel for the worker in the opened connection, it also sets both of them as defaults.
42 | * @return self
43 | */
44 | public function connect();
45 |
46 | /**
47 | * Closes all open channels and connections with RabbitMQ server.
48 | * @return self
49 | */
50 | public function disconnect();
51 |
52 | /**
53 | * Executes `self::disconnect()` and `self::connect()` respectively. Note that this method will not restore old channels.
54 | * @return self
55 | */
56 | public function reconnect();
57 |
58 | /**
59 | * Declares a queue on the default channel of the worker's connection with RabbitMQ server.
60 | * @param array $parameters [optional] The overrides for the default queue options of the worker.
61 | * @param AMQPChannel $_channel [optional] The channel that should be used instead of the default worker's channel.
62 | * @return self
63 | */
64 | public function queue(?array $parameters = null, ?AMQPChannel $_channel = null);
65 |
66 | /**
67 | * Returns the default connection of the worker. If the worker is not connected, it returns null.
68 | * @since 1.1.0
69 | * @return AMQPStreamConnection|null
70 | */
71 | public function getConnection(): ?AMQPStreamConnection;
72 |
73 | /**
74 | * Sets the passed connection as the default connection of the worker.
75 | * @since 1.1.0
76 | * @param AMQPStreamConnection $connection The connection that should be as the default connection of the worker.
77 | * @return self
78 | */
79 | public function setConnection(AMQPStreamConnection $connection);
80 |
81 | /**
82 | * Opens a new connection to RabbitMQ server and returns it. Connections returned by this method pushed to connections array and are not set as default automatically.
83 | * @since 1.1.0
84 | * @param array|null $parameters
85 | * @return AMQPStreamConnection
86 | */
87 | public function getNewConnection(array $parameters = null): AMQPStreamConnection;
88 |
89 | /**
90 | * Returns the default channel of the worker. If the worker is not connected, it returns null.
91 | * @return AMQPChannel|null
92 | */
93 | public function getChannel(): ?AMQPChannel;
94 |
95 | /**
96 | * Sets the passed channel as the default channel of the worker.
97 | * @since 1.1.0
98 | * @param AMQPChannel $channel The channel that should be as the default channel of the worker.
99 | * @return self
100 | */
101 | public function setChannel(AMQPChannel $channel);
102 |
103 | /**
104 | * Returns a new channel on the the passed connection of the worker. If no connection is passed, it uses the default connection. If the worker is not connected, it returns null.
105 | * @param array|null $parameters [optional] The overrides for the default channel options of the worker.
106 | * @param AMQPStreamConnection|null $_connection [optional] The connection that should be used instead of the default worker's connection.
107 | * @return AMQPChannel|null
108 | */
109 | public function getNewChannel(array $parameters = null, ?AMQPStreamConnection $_connection = null): ?AMQPChannel;
110 |
111 | /**
112 | * Fetches a channel object identified by the passed id (channel_id). If not found, it returns null.
113 | * @param int $channelId The id of the channel wished to be fetched.
114 | * @param AMQPStreamConnection|null $_connection [optional] The connection that should be used instead of the default worker's connection.
115 | * @return AMQPChannel|null
116 | */
117 | public function getChannelById(int $channelId): ?AMQPChannel;
118 | }
119 |
--------------------------------------------------------------------------------
/src/Config/AmqpAgentParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Config;
13 |
14 | use PhpAmqpLib\Message\AMQPMessage;
15 | use PhpAmqpLib\Exchange\AMQPExchangeType;
16 | use MAKS\AmqpAgent\Config\AbstractParameters;
17 |
18 | /**
19 | * A class that contains all AMQP Agent parameters as constants.
20 | * @since 1.2.0
21 | */
22 | final class AmqpAgentParameters extends AbstractParameters
23 | {
24 | public const PREFIX = 'maks.amqp.agent.';
25 |
26 | public const COMMAND_PREFIX = '__COMMAND__';
27 |
28 | public const COMMAND_SYNTAX = [
29 | self::COMMAND_PREFIX => [
30 | 'ACTION' => 'OBJECT',
31 | 'PARAMS' => [
32 | 'NAME' => 'VALUE'
33 | ]
34 | ]
35 | ];
36 |
37 | public const CONNECTION_OPTIONS = [
38 | 'host' => 'localhost',
39 | 'port' => 5672,
40 | 'user' => 'guest',
41 | 'password' => 'guest',
42 | 'vhost' => '/',
43 | 'insist' => false,
44 | 'login_method' => 'AMQPLAIN',
45 | 'login_response' => null,
46 | 'locale' => 'en_US',
47 | 'connection_timeout' => 120,
48 | 'read_write_timeout' => 120,
49 | 'context' => null,
50 | 'keepalive' => true,
51 | 'heartbeat' => 60,
52 | 'channel_rpc_timeout' => 120,
53 | 'ssl_protocol' => null
54 | ];
55 |
56 | public const CHANNEL_OPTIONS = [
57 | 'channel_id' => null
58 | ];
59 |
60 | public const QUEUE_OPTIONS = [
61 | 'queue' => self::PREFIX . 'queue',
62 | 'passive' => false,
63 | 'durable' => true,
64 | 'exclusive' => false,
65 | 'auto_delete' => false,
66 | 'nowait' => false,
67 | 'arguments' => [],
68 | 'ticket' => null
69 | ];
70 |
71 | public const EXCHANGE_OPTIONS = [
72 | 'exchange' => self::PREFIX . 'exchange',
73 | 'type' => AMQPExchangeType::HEADERS,
74 | 'passive' => false,
75 | 'durable' => true,
76 | 'auto_delete' => false,
77 | 'internal' => false,
78 | 'nowait' => false,
79 | 'arguments' => [],
80 | 'ticket' => null
81 | ];
82 |
83 | public const BIND_OPTIONS = [
84 | 'queue' => self::PREFIX . 'queue',
85 | 'exchange' => self::PREFIX . 'exchange',
86 | 'routing_key' => self::PREFIX . 'routing',
87 | 'nowait' => false,
88 | 'arguments' => [],
89 | 'ticket' => null
90 | ];
91 |
92 | public const MESSAGE_OPTIONS = [
93 | 'body' => '{}',
94 | 'properties' => [
95 | 'content_type' => 'application/json',
96 | 'content_encoding' => 'UTF-8',
97 | 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT
98 | ]
99 | ];
100 |
101 | public const PUBLISH_OPTIONS = [
102 | 'msg' => null,
103 | 'exchange' => self::PREFIX . 'exchange',
104 | 'routing_key' => self::PREFIX . 'routing',
105 | 'mandatory' => false,
106 | 'immediate' => false,
107 | 'ticket' => null
108 | ];
109 |
110 | public const QOS_OPTIONS = [
111 | 'prefetch_size' => null,
112 | 'prefetch_count' => 5,
113 | 'a_global' => null
114 | ];
115 |
116 | public const WAIT_OPTIONS = [
117 | 'allowed_methods' => null,
118 | 'non_blocking' => true,
119 | 'timeout' => 3600
120 | ];
121 |
122 | public const CONSUME_OPTIONS = [
123 | 'queue' => self::PREFIX . 'queue',
124 | 'consumer_tag' => self::PREFIX . 'consumer',
125 | 'no_local' => false,
126 | 'no_ack' => false,
127 | 'exclusive' => false,
128 | 'nowait' => false,
129 | 'callback' => 'MAKS\AmqpAgent\Helper\Example::callback',
130 | 'ticket' => null,
131 | 'arguments' => []
132 | ];
133 |
134 | public const ACK_OPTIONS = [
135 | 'multiple' => false
136 | ];
137 |
138 | public const NACK_OPTIONS = [
139 | 'multiple' => false,
140 | 'requeue' => true
141 | ];
142 |
143 | public const GET_OPTIONS = [
144 | 'queue' => self::PREFIX . 'queue',
145 | 'no_ack' => false,
146 | 'ticket' => null
147 | ];
148 |
149 | public const CANCEL_OPTIONS = [
150 | 'consumer_tag' => self::PREFIX . 'consumer',
151 | 'nowait' => false,
152 | 'noreturn' => false
153 | ];
154 |
155 | public const RECOVER_OPTIONS = [
156 | 'requeue' => true,
157 | ];
158 |
159 | public const REJECT_OPTIONS = [
160 | 'requeue' => true,
161 | ];
162 |
163 | public const RPC_CONNECTION_OPTIONS = self::CONNECTION_OPTIONS;
164 |
165 | public const RPC_QUEUE_NAME = self::PREFIX . 'maks.amqp.agent.rpc.queue';
166 | }
167 |
--------------------------------------------------------------------------------
/src/Helper/ArrayProxyTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use stdClass;
15 | use ReflectionObject;
16 |
17 | /**
18 | * A trait containing methods for for manipulating and working with arrays.
19 | * @since 2.0.0
20 | */
21 | trait ArrayProxyTrait
22 | {
23 | /**
24 | * Gets a value from an array via dot-notation representation.
25 | * @param array &$array The array to get the value from.
26 | * @param string $key The dotted key representation.
27 | * @param mixed $default [optional] The default fallback value.
28 | * @return mixed The requested value if found otherwise the default parameter.
29 | */
30 | public static function getArrayValueByKey(array &$array, string $key, $default = null)
31 | {
32 | if (!strlen($key) || !count($array)) {
33 | return $default;
34 | }
35 |
36 | $data = &$array;
37 |
38 | if (strpos($key, '.') !== false) {
39 | $parts = explode('.', $key);
40 |
41 | foreach ($parts as $part) {
42 | if (!array_key_exists($part, $data)) {
43 | return $default;
44 | }
45 |
46 | $data = &$data[$part];
47 | }
48 |
49 | return $data;
50 | }
51 |
52 | return array_key_exists($key, $data) ? $data[$key] : $default;
53 | }
54 |
55 | /**
56 | * Sets a value of an array via dot-notation representation.
57 | * @param array $array The array to set the value in.
58 | * @param string $key The string key representation.
59 | * @param mixed $value The value to set.
60 | * @return bool True on success.
61 | */
62 | public static function setArrayValueByKey(array &$array, string $key, $value): bool
63 | {
64 | if (!strlen($key)) {
65 | return false;
66 | }
67 |
68 | $parts = explode('.', $key);
69 | $lastPart = array_pop($parts);
70 |
71 | $data = &$array;
72 |
73 | if (!empty($parts)) {
74 | foreach ($parts as $part) {
75 | if (!isset($data[$part])) {
76 | $data[$part] = [];
77 | }
78 |
79 | $data = &$data[$part];
80 | }
81 | }
82 |
83 | $data[$lastPart] = $value;
84 |
85 | return true;
86 | }
87 |
88 | /**
89 | * Returns a string representation of an array by imploding it recursively with common formatting of data-types.
90 | * @param array $array The array to implode.
91 | * @return string
92 | */
93 | public static function castArrayToString(array $array): string
94 | {
95 | $pieces = [];
96 |
97 | foreach ($array as $item) {
98 | switch (true) {
99 | case (is_array($item)):
100 | $pieces[] = self::castArrayToString($item);
101 | break;
102 | case (is_object($item)):
103 | $pieces[] = get_class($item) ?? 'object';
104 | break;
105 | case (is_string($item)):
106 | $pieces[] = "'{$item}'";
107 | break;
108 | case (is_bool($item)):
109 | $pieces[] = $item ? 'true' : 'false';
110 | break;
111 | case (is_null($item)):
112 | $pieces[] = 'null';
113 | break;
114 | default:
115 | $pieces[] = $item;
116 | }
117 | }
118 |
119 | return '[' . implode(', ', $pieces) . ']';
120 | }
121 |
122 | /**
123 | * Converts (casts) an array to an object (stdClass).
124 | * @param array $array The array to convert.
125 | * @param bool $useJson [optional] Whether to use json_decode/json_encode to cast the array, default is via iteration.
126 | * @return stdClass The result object.
127 | */
128 | public static function castArrayToObject(array $array, bool $useJson = false): stdClass
129 | {
130 | if ($useJson) {
131 | return json_decode(json_encode($array));
132 | }
133 |
134 | $stdClass = new stdClass();
135 |
136 | foreach ($array as $key => $value) {
137 | $stdClass->{$key} = is_array($value)
138 | ? self::castArrayToObject($value, $useJson)
139 | : $value;
140 | }
141 |
142 | return $stdClass;
143 | }
144 |
145 | /**
146 | * Converts (casts) an object to an associative array.
147 | * @param object $object The object to convert.
148 | * @param bool $useJson [optional] Whether to use json_decode/json_encode to cast the object, default is via reflection.
149 | * @return array The result array.
150 | */
151 | public static function castObjectToArray($object, bool $useJson = false): array
152 | {
153 | if ($useJson) {
154 | return json_decode(json_encode($object), true);
155 | }
156 |
157 | $array = [];
158 |
159 | $reflectionClass = new ReflectionObject($object);
160 | foreach ($reflectionClass->getProperties() as $property) {
161 | $property->setAccessible(true);
162 | $array[$property->getName()] = $property->getValue($object);
163 | $property->setAccessible(false);
164 | }
165 |
166 | return $array;
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/RPC/ServerEndpoint.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\RPC;
13 |
14 | use PhpAmqpLib\Message\AMQPMessage;
15 | use MAKS\AmqpAgent\Helper\ClassProxy;
16 | use MAKS\AmqpAgent\RPC\AbstractEndpoint;
17 | use MAKS\AmqpAgent\RPC\ServerEndpointInterface;
18 | use MAKS\AmqpAgent\Exception\RPCEndpointException;
19 |
20 | /**
21 | * A class specialized in responding. Implementing only the methods needed for a server.
22 | *
23 | * Example:
24 | * ```
25 | * $serverEndpoint = new ServerEndpoint();
26 | * $serverEndpoint->on('some.event', function () { ... });
27 | * $serverEndpoint->connect();
28 | * $serverEndpoint->respond('Namespace\SomeClass::someMethod', 'queue.name');
29 | * $serverEndpoint->disconnect();
30 | * ```
31 | *
32 | * @since 2.0.0
33 | * @api
34 | */
35 | class ServerEndpoint extends AbstractEndpoint implements ServerEndpointInterface
36 | {
37 | /**
38 | * The callback to use when processing the requests.
39 | * @var callable
40 | */
41 | protected $callback;
42 |
43 |
44 | /**
45 | * Listens on requests coming via the passed queue and processes them with the passed callback.
46 | * @param callable|null $callback [optional] The callback to process the request. This callback will be passed an `AMQPMessage` and must return a string.
47 | * @param string|null $queueName [optional] The name of the queue to listen on.
48 | * @return string The last processed request.
49 | * @throws RPCEndpointException If the server is not connected yet or if the passed callback didn't return a string.
50 | */
51 | public function respond(?callable $callback = null, ?string $queueName = null): string
52 | {
53 | $this->callback = $callback ?? [$this, 'callback'];
54 | $this->queueName = $queueName ?? $this->queueName;
55 |
56 | if ($this->isConnected()) {
57 | $this->requestQueue = $this->queueName;
58 |
59 | $this->channel->queue_declare(
60 | $this->requestQueue,
61 | false,
62 | false,
63 | false,
64 | false
65 | );
66 |
67 | $this->channel->basic_qos(
68 | null,
69 | 1,
70 | null
71 | );
72 |
73 | $this->channel->basic_consume(
74 | $this->requestQueue,
75 | null,
76 | false,
77 | false,
78 | false,
79 | false,
80 | function ($message) {
81 | ClassProxy::call($this, 'onRequest', $message);
82 | }
83 | );
84 |
85 | while ($this->channel->is_consuming()) {
86 | $this->channel->wait();
87 | }
88 |
89 | return $this->requestBody;
90 | }
91 |
92 | throw new RPCEndpointException('Server is not connected yet!');
93 | }
94 |
95 | /**
96 | * Listens on requests coming via the passed queue and processes them with the passed callback.
97 | * Alias for `self::respond()`.
98 | * @param callable|null $callback [optional] The callback to process the request. This callback will be passed an `AMQPMessage` and must return a string.
99 | * @param string|null $queueName [optional] The queue to listen on.
100 | * @return string The last processed request.
101 | * @throws RPCEndpointException If the server is not connected yet or if the passed callback didn't return a string.
102 | */
103 | public function serve(?callable $callback = null, ?string $queueName = null): string
104 | {
105 | return $this->respond($callback, $queueName);
106 | }
107 |
108 | /**
109 | * Replies to the client.
110 | * @param AMQPMessage $request
111 | * @return void
112 | * @throws RPCEndpointException
113 | */
114 | protected function onRequest(AMQPMessage $request): void
115 | {
116 | $this->trigger('request.on.get', [$request]);
117 |
118 | $this->requestBody = $request->body;
119 | $this->responseBody = call_user_func($this->callback, $request);
120 | $this->responseQueue = (string)$request->get('reply_to');
121 | $this->correlationId = (string)$request->get('correlation_id');
122 |
123 | if (!is_string($this->responseBody)) {
124 | throw new RPCEndpointException(
125 | sprintf(
126 | 'The passed processing callback must return a string, instead it returned (data-type: %s)!',
127 | gettype($this->responseBody)
128 | )
129 | );
130 | }
131 |
132 | $message = new AMQPMessage($this->responseBody);
133 | $message->set('correlation_id', $this->correlationId);
134 | $message->set('timestamp', time());
135 |
136 | $this->trigger('response.before.send', [$message]);
137 |
138 | $request->getChannel()->basic_publish(
139 | $message,
140 | null,
141 | $this->responseQueue
142 | );
143 |
144 | $request->ack();
145 |
146 | $this->trigger('response.after.send', [$message]);
147 | }
148 |
149 | /**
150 | * Returns the final request body. This method will be ignored if a callback in `self::respond()` is specified.
151 | * @param AMQPMessage $message
152 | * @return string
153 | */
154 | protected function callback(AMQPMessage $message): string
155 | {
156 | return $message->body;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/RPC/ClientEndpoint.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\RPC;
13 |
14 | use PhpAmqpLib\Message\AMQPMessage;
15 | use MAKS\AmqpAgent\Helper\ClassProxy;
16 | use MAKS\AmqpAgent\Helper\IDGenerator;
17 | use MAKS\AmqpAgent\RPC\AbstractEndpoint;
18 | use MAKS\AmqpAgent\RPC\ClientEndpointInterface;
19 | use MAKS\AmqpAgent\Exception\RPCEndpointException;
20 |
21 | /**
22 | * A class specialized in requesting. Implementing only the methods needed for a client.
23 | *
24 | * Example:
25 | * ```
26 | * $clientEndpoint = new ClientEndpoint();
27 | * $clientEndpoint->on('some.event', function () { ... });
28 | * $clientEndpoint->connect();
29 | * $clientEndpoint->request('Message Body', 'queue.name');
30 | * $clientEndpoint->disconnect();
31 | * ```
32 | *
33 | * @since 2.0.0
34 | * @api
35 | */
36 | class ClientEndpoint extends AbstractEndpoint implements ClientEndpointInterface
37 | {
38 | /**
39 | * Opens a connection with RabbitMQ server.
40 | * @param array|null $connectionOptions
41 | * @return self
42 | * @throws RPCEndpointException
43 | */
44 | public function connect(?array $connectionOptions = [])
45 | {
46 | parent::connect($connectionOptions);
47 |
48 | if ($this->isConnected()) {
49 | list($this->responseQueue, , ) = $this->channel->queue_declare(
50 | null,
51 | false,
52 | false,
53 | true,
54 | false
55 | );
56 |
57 | $this->channel->basic_consume(
58 | $this->responseQueue,
59 | null,
60 | false,
61 | false,
62 | false,
63 | false,
64 | function ($message) {
65 | ClassProxy::call($this, 'onResponse', $message);
66 | }
67 | );
68 | }
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * Sends the passed request to the server using the passed queue.
75 | * @param string|AMQPMessage $request The request body or an `AMQPMessage` instance.
76 | * @param string|null $queueName [optional] The name of queue to send through.
77 | * @return string The response body.
78 | * @throws RPCEndpointException If the client is not connected yet or if request Correlation ID does not match the one of the response.
79 | */
80 | public function request($request, ?string $queueName = null): string
81 | {
82 | if (!$this->isConnected()) {
83 | throw new RPCEndpointException('Client is not connected yet!');
84 | }
85 |
86 | $this->queueName = $queueName ?? $this->queueName;
87 | $this->requestBody = $request instanceof AMQPMessage ? $request->body : (string)$request;
88 | $this->responseBody = null;
89 | $this->requestQueue = $this->queueName;
90 | $this->correlationId = IDGenerator::generateHash();
91 |
92 | $message = $request instanceof AMQPMessage ? $request : new AMQPMessage((string)$request);
93 | $message->set('reply_to', $this->responseQueue);
94 | $message->set('correlation_id', $this->correlationId);
95 | $message->set('timestamp', time());
96 |
97 | $this->channel->queue_declare(
98 | $this->requestQueue,
99 | false,
100 | false,
101 | false,
102 | false
103 | );
104 |
105 | $this->trigger('request.before.send', [$message]);
106 |
107 | $this->channel->basic_publish(
108 | $message,
109 | null,
110 | $this->requestQueue
111 | );
112 |
113 | $this->trigger('request.after.send', [$message]);
114 |
115 | while ($this->responseBody === null) {
116 | $this->channel->wait();
117 | }
118 |
119 | return $this->responseBody;
120 | }
121 |
122 | /**
123 | * Sends the passed request to the server using the passed queue.
124 | * Alias for `self::request()`.
125 | * @param string|AMQPMessage $request The request body or an `AMQPMessage` instance.
126 | * @param string|null $queueName [optional] The name of queue to send through.
127 | * @return string The response body.
128 | * @throws RPCEndpointException If the client is not connected yet or if request Correlation ID does not match the one of the response.
129 | */
130 | public function call($request, ?string $queueName = null): string
131 | {
132 | return $this->request($request, $queueName);
133 | }
134 |
135 | /**
136 | * Validates the response.
137 | * @param AMQPMessage $response
138 | * @return void
139 | * @throws RPCEndpointException
140 | */
141 | protected function onResponse(AMQPMessage $response): void
142 | {
143 | $this->trigger('response.on.get', [$response]);
144 |
145 | if ($this->correlationId === $response->get('correlation_id')) {
146 | $this->responseBody = $this->callback($response);
147 | $response->ack();
148 | return;
149 | }
150 |
151 | throw new RPCEndpointException(
152 | sprintf(
153 | 'Correlation ID of the response "%s" does not match the one of the request "%s"!',
154 | $this->correlationId,
155 | (string)$response->get('correlation_id')
156 | )
157 | );
158 | }
159 |
160 | /**
161 | * Returns the final response body.
162 | * @param AMQPMessage $message
163 | * @return string
164 | */
165 | protected function callback(AMQPMessage $message): string
166 | {
167 | return $message->body;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/Worker/ConsumerInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Connection\AMQPStreamConnection;
15 | use PhpAmqpLib\Channel\AMQPChannel;
16 | use PhpAmqpLib\Message\AMQPMessage;
17 | use MAKS\AmqpAgent\Worker\AbstractWorkerInterface;
18 |
19 | /**
20 | * An interface defining the basic methods of a consumer.
21 | * @since 1.0.0
22 | */
23 | interface ConsumerInterface extends AbstractWorkerInterface
24 | {
25 | /**
26 | * Acknowledges an AMQP message object.
27 | * Starting from v1.1.1, you can use php-amqplib AMQPMessage::ack() method instead.
28 | * @param AMQPMessage $_message The message object that should be acknowledged.
29 | * @param array|null $parameters [optional] The overrides for the default acknowledge options.
30 | * @return void
31 | */
32 | public static function ack(AMQPMessage $_message, ?array $parameters = null): void;
33 |
34 | /**
35 | * Unacknowledges an AMQP message object.
36 | * Starting from v1.1.1, you can use php-amqplib AMQPMessage::nack() method instead.
37 | * @param AMQPChannel|null $_channel [optional] The channel that should be used. The method will try using the channel attached with the message if no channel was specified, although there is no guarantee this will work as this depends on the way the message was fetched.
38 | * @param AMQPMessage $_message The message object that should be unacknowledged.
39 | * @param array|null $parameters [optional] The overrides for the default exchange options.
40 | * @return void
41 | */
42 | public static function nack(?AMQPChannel $_channel, AMQPMessage $_message, ?array $parameters = null): void;
43 |
44 | /**
45 | * Gets a message object from a channel, direct access to a queue.
46 | * @deprecated 1.0.0 Direct queue access is not recommended. Use `self::consume()` instead.
47 | * @param AMQPChannel $_channel The channel that should be used.
48 | * @param array|null $parameters [optional] The overrides for the default get options.
49 | * @return AMQPMessage|null
50 | */
51 | public static function get(AMQPChannel $_channel, ?array $parameters = null): ?AMQPMessage;
52 |
53 | /**
54 | * Ends a queue consumer.
55 | * @param AMQPChannel $_channel The channel that should be used.
56 | * @param array|null $parameters [optional] The overrides for the default cancel options.
57 | * @return mixed
58 | */
59 | public static function cancel(AMQPChannel $_channel, ?array $parameters = null);
60 |
61 | /**
62 | * Redelivers unacknowledged messages
63 | * @param AMQPChannel $_channel The channel that should be used.
64 | * @param array|null $parameters [optional] The overrides for the default recover options.
65 | * @return mixed
66 | */
67 | public static function recover(AMQPChannel $_channel, ?array $parameters = null);
68 |
69 | /**
70 | * Rejects an AMQP message object.
71 | * @deprecated Starting from v1.1.1, you can use php-amqplib native AMQPMessage::reject() method instead.
72 | * @param AMQPChannel $_channel The channel that should be used.
73 | * @param AMQPMessage $_message The message object that should be rejected.
74 | * @param array|null $parameters [optional] The overrides for the default reject options.
75 | * @return void
76 | */
77 | public static function reject(AMQPChannel $_channel, AMQPMessage $_message, ?array $parameters = null): void;
78 |
79 |
80 | /**
81 | * Specifies the quality of service on the default channel of the worker's connection to RabbitMQ server.
82 | * @param array|null $parameters [optional] The overrides for the default quality of service options of the worker.
83 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
84 | * @return self
85 | */
86 | public function qos(?array $parameters = null, ?AMQPChannel $_channel = null);
87 |
88 | /**
89 | * Consumes messages from the default channel of the worker's connection to RabbitMQ server.
90 | * @param callback|array|string|null $callback [optional] The callback that the consumer uses to process the messages.
91 | * @param array|null $variables [optional] The variables that should be passed to the callback.
92 | * @param array|null $parameters [optional] The overrides for the default exchange options of the worker.
93 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
94 | * @return self
95 | */
96 | public function consume($callback = null, ?array $variables = null, ?array $parameters = null, ?AMQPChannel $_channel = null);
97 |
98 | /**
99 | * Checks whether the default channel is consuming.
100 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
101 | * @return bool
102 | */
103 | public function isConsuming(?AMQPChannel $_channel = null): bool;
104 |
105 | /**
106 | * Keeps the connection to RabbitMQ server alive as long as the default channel is in used.
107 | * @param array|null $parameters [optional] The overrides for the default exchange options of the worker.
108 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
109 | * @return self
110 | */
111 | public function wait(?array $parameters = null, ?AMQPChannel $_channel = null);
112 |
113 | /**
114 | * Tries to keep the connection to RabbitMQ server alive as long as there are channels in used (default or not).
115 | * @param array|null $parameters [optional] The overrides for the default exchange options of the worker.
116 | * @param AMQPStreamConnection|null $_connection [optional] The connection that should be used instead of the default worker's connection.
117 | * @return self
118 | */
119 | public function waitForAll(?array $parameters = null, ?AMQPStreamConnection $_connection = null);
120 | }
121 |
--------------------------------------------------------------------------------
/src/Config.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent;
13 |
14 | use Exception;
15 | use MAKS\AmqpAgent\Helper\ArrayProxy;
16 | use MAKS\AmqpAgent\Exception\ConfigFileNotFoundException;
17 |
18 | /**
19 | * A class that turns the configuration file into an object.
20 | *
21 | * Example:
22 | * ```
23 | * $config = new Config('path/to/some/config-file.php'); // specific config
24 | * $config = new Config(); // default config
25 | * ```
26 | *
27 | * @since 1.0.0
28 | * @property array $connectionOptions
29 | * @property array $channelOptions
30 | * @property array $queueOptions
31 | * @property array $exchangeOptions
32 | * @property array $bindOptions
33 | * @property array $qosOptions
34 | * @property array $waitOptions
35 | * @property array $messageOptions
36 | * @property array $publishOptions
37 | * @property array $consumeOptions
38 | * @property array $rpcConnectionOptions
39 | * @property string $rpcQueueName
40 | */
41 | final class Config
42 | {
43 | /**
44 | * The default name of the configuration file.
45 | * @var string
46 | */
47 | public const DEFAULT_CONFIG_FILE_NAME = 'maks-amqp-agent-config';
48 |
49 | /**
50 | * The default path of the configuration file.
51 | * @var string
52 | */
53 | public const DEFAULT_CONFIG_FILE_PATH = __DIR__ . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . self::DEFAULT_CONFIG_FILE_NAME . '.php';
54 |
55 | /**
56 | * The multidimensional configuration array.
57 | * @var array
58 | */
59 | private $config;
60 |
61 | /**
62 | * Configuration file path.
63 | * @var string
64 | */
65 | private $configPath;
66 |
67 |
68 | /**
69 | * Config object constructor.
70 | * @param string|null $configPath [optional] The path to AMQP Agent configuration file.
71 | * @throws ConfigFileNotFoundException
72 | */
73 | public function __construct(?string $configPath = null)
74 | {
75 | $configFile = realpath($configPath ?? self::DEFAULT_CONFIG_FILE_PATH);
76 |
77 | if (!$configFile || !file_exists($configFile)) {
78 | throw new ConfigFileNotFoundException(
79 | "AMQP Agent configuration file cloud not be found, check if the given path \"{$configPath}\" exists."
80 | );
81 | }
82 |
83 | $this->config = include($configFile);
84 | $this->configPath = $configFile;
85 |
86 | $this->repair();
87 | }
88 |
89 | /**
90 | * Gets the the given key from the configuration array via public property access notation.
91 | * @param string $key
92 | * @return mixed
93 | */
94 | public function __get(string $key)
95 | {
96 | return $this->config[$key];
97 | }
98 |
99 | /**
100 | * Sets the the given key in the configuration array via public property assignment notation.
101 | * @param string $key
102 | * @param mixed $value
103 | * @return void
104 | */
105 | public function __set(string $key, $value)
106 | {
107 | $this->config[$key] = $value;
108 | }
109 |
110 | /**
111 | * Returns config file path if the object was casted to a string.
112 | * @return string
113 | */
114 | public function __toString()
115 | {
116 | return $this->configPath;
117 | }
118 |
119 |
120 | /**
121 | * Repairs the config array if first-level of the passed array does not have all keys.
122 | * @return void
123 | */
124 | private function repair(): void
125 | {
126 | $config = require(self::DEFAULT_CONFIG_FILE_PATH);
127 |
128 | foreach ($config as $key => $value) {
129 | if (!array_key_exists($key, $this->config)) {
130 | $this->config[$key] = [];
131 | }
132 | }
133 |
134 | unset($config);
135 | }
136 |
137 | /**
138 | * Checks whether a value exists in the configuration array via dot-notation representation.
139 | * @since 1.2.2
140 | * @param string $key The dotted key representation.
141 | * @return bool True if key is set otherwise false.
142 | */
143 | public function has(string $key): bool
144 | {
145 | $value = ArrayProxy::getArrayValueByKey($this->config, $key, null);
146 |
147 | return isset($value);
148 | }
149 |
150 | /**
151 | * Gets a value of a key from the configuration array via dot-notation representation.
152 | * @since 1.2.2
153 | * @param string $key The dotted key representation.
154 | * @return mixed The requested value or null.
155 | */
156 | public function get(string $key)
157 | {
158 | $value = ArrayProxy::getArrayValueByKey($this->config, $key);
159 |
160 | return $value;
161 | }
162 |
163 | /**
164 | * Sets a value of a key from the configuration array via dot-notation representation.
165 | * @since 1.2.2
166 | * @param string $key The dotted key representation.
167 | * @param mixed $value The value to set.
168 | * @return self
169 | */
170 | public function set(string $key, $value)
171 | {
172 | ArrayProxy::setArrayValueByKey($this->config, $key, $value);
173 |
174 | return $this;
175 | }
176 |
177 | /**
178 | * Returns the default configuration array.
179 | * @return array
180 | */
181 | public function getDefaultConfig(): array
182 | {
183 | return include(self::DEFAULT_CONFIG_FILE_PATH);
184 | }
185 |
186 | /**
187 | * Returns the current configuration array.
188 | * @return array
189 | */
190 | public function getConfig(): array
191 | {
192 | return $this->config;
193 | }
194 |
195 | /**
196 | * Sets a new configuration array to be used instead of the current.
197 | * @param array $config
198 | * @return self
199 | */
200 | public function setConfig(array $config)
201 | {
202 | $this->config = $config;
203 |
204 | $this->repair();
205 |
206 | return $this;
207 | }
208 |
209 | /**
210 | * Returns the path of the configuration file.
211 | * @return string
212 | */
213 | public function getConfigPath(): string
214 | {
215 | return $this->configPath;
216 | }
217 |
218 | /**
219 | * Sets the path of the configuration file and rebuilds the internal state of the object.
220 | * @param string $configPath
221 | * @return self
222 | * @throws ConfigFileNotFoundException
223 | */
224 | public function setConfigPath(string $configPath)
225 | {
226 | try {
227 | $this->config = include($configPath);
228 | $this->configPath = $configPath;
229 |
230 | $this->repair();
231 | } catch (Exception $error) {
232 | throw new ConfigFileNotFoundException(
233 | "Something went wrong when trying to include the file and rebuild the configuration, check if the given path \"{$configPath}\" exists.",
234 | (int)$error->getCode(),
235 | $error
236 | );
237 | }
238 |
239 | return $this;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/Helper/Logger.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use MAKS\AmqpAgent\Helper\Utility;
15 |
16 | /**
17 | * A class to write logs, exposing methods that work statically and on instantiation.
18 | * This class DOES NOT implement `Psr\Log\LoggerInterface`.
19 | *
20 | * Example:
21 | * ```
22 | * // static
23 | * Logger::log('Some message to log.', 'filename', 'path/to/some/directory');
24 | * // instantiated
25 | * $logger = new Logger();
26 | * $logger->setFilename('filename');
27 | * $logger->setDirectory('path/to/some/directory');
28 | * $logger->write('Some message to log.');
29 | * ```
30 | *
31 | * @since 1.0.0
32 | */
33 | class Logger
34 | {
35 | /**
36 | * The filename of the log file.
37 | * @var string
38 | */
39 | public $filename;
40 |
41 | /**
42 | * The directory where the log file gets written.
43 | * @var string
44 | */
45 | public $directory;
46 |
47 |
48 | /**
49 | * Passing null for $directory will raise a warning and force the logger to find a reasonable directory to write the file in.
50 | * @param string|null $filename The name wished to be given to the file. Pass null for auto-generate.
51 | * @param string|null $directory The directory where the log file should be written.
52 | */
53 | public function __construct(?string $filename, ?string $directory)
54 | {
55 | $this->filename = $filename;
56 | $this->directory = $directory;
57 | }
58 |
59 |
60 | /**
61 | * Logs a message to a file, generates it if it does not exist and raises a user-level warning and/or notice on misuse.
62 | * @param string $message The message wished to be logged.
63 | * @return bool True on success.
64 | */
65 | public function write(string $message): bool
66 | {
67 | return self::log($message, $this->filename, $this->directory);
68 | }
69 |
70 | /**
71 | * Gets filename property.
72 | * @return string
73 | */
74 | public function getFilename()
75 | {
76 | return $this->filename;
77 | }
78 |
79 | /**
80 | * Sets filename property.
81 | * @param string $filename The filename.
82 | * @return self
83 | */
84 | public function setFilename(string $filename)
85 | {
86 | $this->filename = $filename;
87 | return $this;
88 | }
89 |
90 | /**
91 | * Gets directory property.
92 | * @return string
93 | */
94 | public function getDirectory()
95 | {
96 | return $this->directory;
97 | }
98 |
99 | /**
100 | * Sets directory property.
101 | * @param string $directory The directory.
102 | * @return self
103 | */
104 | public function setDirectory(string $directory)
105 | {
106 | $this->directory = $directory;
107 | return $this;
108 | }
109 |
110 |
111 | /**
112 | * Logs a message to a file, generates it if it does not exist and raises a user-level warning and/or notice on misuse.
113 | * @param string $message The message wished to be logged.
114 | * @param string|null $filename [optional] The name wished to be given to the file. If not provided a Notice will be raised with the auto-generated filename.
115 | * @param string|null $directory [optional] The directory where the log file should be written. If not provided a Warning will be raised with the used path.
116 | * @return bool True if message was written.
117 | */
118 | public static function log(string $message, ?string $filename = null, ?string $directory = null): bool
119 | {
120 | $passed = false;
121 |
122 | if (null === $filename) {
123 | $filename = self::getFallbackFilename();
124 | Utility::emit(
125 | [
126 | 'yellow' => sprintf('%s() was called without specifying a filename.', __METHOD__),
127 | 'green' => sprintf('Log file will be named: "%s".', $filename)
128 | ],
129 | null,
130 | E_USER_NOTICE
131 | );
132 | }
133 |
134 | if (null === $directory) {
135 | $directory = self::getFallbackDirectory();
136 | Utility::emit(
137 | [
138 | 'yellow' => sprintf('%s() was called without specifying a directory.', __METHOD__),
139 | 'red' => sprintf('Log file will be written in: "%s".', $directory)
140 | ],
141 | null,
142 | E_USER_WARNING
143 | );
144 | }
145 |
146 | $file = self::getNormalizedPath($directory, $filename);
147 |
148 | // create log file if it does not exist
149 | if (!is_file($file) && is_writable($directory)) {
150 | $signature = 'Created by ' . __METHOD__ . date('() \o\\n l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL;
151 | file_put_contents($file, $signature, 0, stream_context_create());
152 | chmod($file, 0775);
153 | }
154 |
155 | // write in the log file
156 | if (is_writable($file)) {
157 | // empty the the file if it exceeds 64MB
158 | // @codeCoverageIgnoreStart
159 | clearstatcache(true, $file);
160 | if (filesize($file) > 6.4e+7) {
161 | $stream = fopen($file, 'r');
162 | if (is_resource($stream)) {
163 | $signature = fgets($stream) . 'For exceeding 64MB, it was overwritten on ' . date('l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL;
164 | fclose($stream);
165 | file_put_contents($file, $signature, 0, stream_context_create());
166 | chmod($file, 0775);
167 | }
168 | }
169 | // @codeCoverageIgnoreEnd
170 |
171 | $timestamp = Utility::time()->format(DATE_ISO8601);
172 | $log = $timestamp . ' ' . $message . PHP_EOL;
173 |
174 | $stream = fopen($file, 'a+');
175 | if (is_resource($stream)) {
176 | fwrite($stream, $log);
177 | fclose($stream);
178 | $passed = true;
179 | }
180 | }
181 |
182 | return $passed;
183 | }
184 |
185 | /**
186 | * Returns a fallback filename based on date.
187 | * @since 1.2.1
188 | * @return string
189 | */
190 | protected static function getFallbackFilename(): string
191 | {
192 | return 'maks-amqp-agent-log-' . date("Ymd");
193 | }
194 |
195 | /**
196 | * Returns a fallback writing directory based on caller.
197 | * @since 1.2.1
198 | * @return string
199 | */
200 | protected static function getFallbackDirectory(): string
201 | {
202 | $backtrace = Utility::backtrace(['file'], 0);
203 | $fallback1 = strlen($_SERVER["DOCUMENT_ROOT"]) ? $_SERVER["DOCUMENT_ROOT"] : null;
204 | $fallback2 = isset($backtrace['file']) ? dirname($backtrace['file']) : __DIR__;
205 |
206 | return $fallback1 ?? $fallback2;
207 | }
208 |
209 | /**
210 | * Returns a normalized path based on OS.
211 | * @since 1.2.1
212 | * @param string $directory The directory.
213 | * @param string $filename The Filename.
214 | * @return string The full normalized path.
215 | */
216 | protected static function getNormalizedPath(string $directory, string $filename): string
217 | {
218 | $ext = '.log';
219 | $filename = substr($filename, -strlen($ext)) === $ext ? $filename : $filename . $ext;
220 | $directory = $directory . DIRECTORY_SEPARATOR;
221 | $path = $directory . $filename;
222 |
223 | return preg_replace("/\/+|\\+/", DIRECTORY_SEPARATOR, $path);
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent;
13 |
14 | use MAKS\AmqpAgent\Config;
15 | use MAKS\AmqpAgent\Worker\Publisher;
16 | use MAKS\AmqpAgent\Worker\Consumer;
17 | use MAKS\AmqpAgent\RPC\ClientEndpoint;
18 | use MAKS\AmqpAgent\RPC\ServerEndpoint;
19 | use MAKS\AmqpAgent\Helper\ArrayProxy;
20 | use MAKS\AmqpAgent\Helper\Serializer;
21 | use MAKS\AmqpAgent\Helper\Logger;
22 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
23 |
24 | /**
25 | * A class returns everything AMQP Agent has to offer. A simple service container so to say.
26 | *
27 | * Example:
28 | * ```
29 | * $config = new Config('path/to/some/config-file.php');
30 | * $client = new Client($config);
31 | * $publisher = $client->getPublisher(); // or $client->get('publisher');
32 | * $consumer = $client->getConsumer(); // or $client->get('consumer');
33 | * ```
34 | *
35 | * @since 1.0.0
36 | * @api
37 | */
38 | class Client
39 | {
40 | /**
41 | * An instance of the configuration object.
42 | * @var Config
43 | */
44 | protected $config;
45 |
46 | /**
47 | * An instance of the Publisher class.
48 | * @var Publisher
49 | */
50 | protected $publisher;
51 |
52 | /**
53 | * An instance of the Consumer class.
54 | * @var Consumer
55 | */
56 | protected $consumer;
57 |
58 | /**
59 | * An instance of the RPC Client class.
60 | * @var ClientEndpoint
61 | */
62 | protected $clientEndpoint;
63 |
64 | /**
65 | * An instance of the RPC Server class.
66 | * @var ServerEndpoint
67 | */
68 | protected $serverEndpoint;
69 |
70 | /**
71 | * An instance of the Serializer class.
72 | * @var Serializer
73 | */
74 | protected $serializer;
75 |
76 | /**
77 | * An instance of the Logger class.
78 | * @var Logger
79 | */
80 | protected $logger;
81 |
82 |
83 | /**
84 | * Client object constructor.
85 | * @param Config|string $config An instance of the Config class or a path to a config file.
86 | * @throws AmqpAgentException
87 | */
88 | public function __construct($config)
89 | {
90 | if ($config instanceof Config) {
91 | $this->config = $config;
92 | } elseif (is_string($config) && strlen(trim($config)) > 0) {
93 | $this->config = new Config($config);
94 | } else {
95 | throw new AmqpAgentException(
96 | 'A Config instance or a valid path to a config file must be specified.'
97 | );
98 | }
99 | }
100 |
101 | /**
102 | * Gets a class member via public property access notation.
103 | * @param string $member Property name.
104 | * @return mixed
105 | * @throws AmqpAgentException
106 | */
107 | public function __get(string $member)
108 | {
109 | // using $this->get() to reuse the logic in get() method.
110 | return $this->get($member);
111 | }
112 |
113 |
114 | /**
115 | * Returns an instance of a class by its name (lowercase, UPPERCASE, PascalCase, camelCase, dot.case, kebab-case, or snake_case representation of class name).
116 | * @param string $member Member name. Check out `self::gettable()` for available members.
117 | * @return Config|Publisher|Consumer|Serializer|Logger
118 | * @throws AmqpAgentException
119 | */
120 | public function get(string $member)
121 | {
122 | $method = __FUNCTION__ . preg_replace('/[\.\-_]+/', '', ucwords(strtolower($member), '.-_'));
123 |
124 | if (method_exists($this, $method)) {
125 | return $this->{$method}();
126 | }
127 |
128 | $available = ArrayProxy::castArrayToString($this->gettable());
129 | throw new AmqpAgentException(
130 | "The requested member with the name \"{$member}\" does not exist! Available members are: {$available}."
131 | );
132 | }
133 |
134 |
135 | /**
136 | * Returns an array of available members that can be obtained via `self::get()`.
137 | * @since 1.2.1
138 | * @return array
139 | */
140 | public static function gettable(): array
141 | {
142 | $methods = get_class_methods(static::class);
143 | $gettable = [];
144 | $separator = ('.-_')[rand(0, 2)];
145 |
146 | foreach ($methods as $method) {
147 | if (preg_match('/get[A-Z][a-z]+/', $method)) {
148 | $gettable[] = strtolower(
149 | preg_replace(
150 | ['/get/', '/([a-z])([A-Z])/'],
151 | ['', '$1' . $separator . '$2'],
152 | $method
153 | )
154 | );
155 | }
156 | }
157 |
158 | return $gettable;
159 | }
160 |
161 |
162 | /**
163 | * Returns an instance of the Publisher class.
164 | * @return Publisher
165 | * @api
166 | */
167 | public function getPublisher(): Publisher
168 | {
169 | if (!isset($this->publisher)) {
170 | $this->publisher = new Publisher(
171 | $this->config->connectionOptions,
172 | $this->config->channelOptions,
173 | $this->config->queueOptions,
174 | $this->config->exchangeOptions,
175 | $this->config->bindOptions,
176 | $this->config->messageOptions,
177 | $this->config->publishOptions
178 | );
179 | }
180 |
181 | return $this->publisher;
182 | }
183 |
184 | /**
185 | * Returns an instance of the Consumer class.
186 | * @return Consumer
187 | */
188 | public function getConsumer(): Consumer
189 | {
190 | if (!isset($this->consumer)) {
191 | $this->consumer = new Consumer(
192 | $this->config->connectionOptions,
193 | $this->config->channelOptions,
194 | $this->config->queueOptions,
195 | $this->config->qosOptions,
196 | $this->config->waitOptions,
197 | $this->config->consumeOptions
198 | );
199 | }
200 |
201 | return $this->consumer;
202 | }
203 |
204 | /**
205 | * Returns an instance of the RPC Client class.
206 | * @return ClientEndpoint
207 | */
208 | public function getClientEndpoint(): ClientEndpoint
209 | {
210 | if (!isset($this->clientEndpoint)) {
211 | $this->clientEndpoint = new ClientEndpoint(
212 | $this->config->rpcConnectionOptions,
213 | $this->config->rpcQueueName
214 | );
215 | }
216 |
217 | return $this->clientEndpoint;
218 | }
219 |
220 | /**
221 | * Returns an instance of the RPC Server class.
222 | * @return ServerEndpoint
223 | */
224 | public function getServerEndpoint(): ServerEndpoint
225 | {
226 | if (!isset($this->serverEndpoint)) {
227 | $this->serverEndpoint = new ServerEndpoint(
228 | $this->config->rpcConnectionOptions,
229 | $this->config->rpcQueueName
230 | );
231 | }
232 |
233 | return $this->serverEndpoint;
234 | }
235 |
236 | /**
237 | * Returns an instance of the Serializer class.
238 | * @return Serializer
239 | */
240 | public function getSerializer(): Serializer
241 | {
242 | if (!isset($this->serializer)) {
243 | $this->serializer = new Serializer();
244 | }
245 |
246 | return $this->serializer;
247 | }
248 |
249 | /**
250 | * Returns an instance of the Logger class.
251 | * Filename and directory must be set through setters.
252 | * @return Logger
253 | */
254 | public function getLogger(): Logger
255 | {
256 | if (!isset($this->logger)) {
257 | $this->logger = new Logger(null, null);
258 | }
259 |
260 | return $this->logger;
261 | }
262 |
263 | /**
264 | * Returns the currently used config object.
265 | * @return Config
266 | */
267 | public function getConfig(): Config
268 | {
269 | return $this->config;
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/Helper/ClassProxyTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use Closure;
15 | use Exception;
16 | use ReflectionClass;
17 | use ReflectionObject;
18 | use ReflectionException;
19 | use MAKS\AmqpAgent\Exception\AmqpAgentException;
20 |
21 | /**
22 | * A trait containing methods for proxy methods calling, properties manipulation, and class utilities.
23 | * @since 2.0.0
24 | */
25 | trait ClassProxyTrait
26 | {
27 | /**
28 | * Calls a private, protected, or public method on an object.
29 | * @param object $object Class instance.
30 | * @param string $method Method name.
31 | * @param mixed ...$arguments
32 | * @return mixed The function result, or false on error.
33 | * @throws AmqpAgentException On failure or if the called function threw an exception.
34 | */
35 | public static function callMethod($object, string $method, ...$arguments)
36 | {
37 | return call_user_func(
38 | Closure::bind(
39 | function () use ($object, $method, $arguments) {
40 | try {
41 | return call_user_func_array(
42 | array(
43 | $object,
44 | $method
45 | ),
46 | $arguments
47 | );
48 | } catch (Exception $error) {
49 | AmqpAgentException::rethrow($error, sprintf('%s::%s() failed!', static::class, __FUNCTION__), false);
50 | }
51 | },
52 | null,
53 | $object
54 | )
55 | );
56 | }
57 |
58 | /**
59 | * Gets a private, protected, or public property (default, static, or constant) of an object.
60 | * @param object $object Class instance.
61 | * @param string $property Property name.
62 | * @return mixed The property value.
63 | * @throws AmqpAgentException On failure.
64 | */
65 | public static function getProperty($object, string $property)
66 | {
67 | return call_user_func(
68 | Closure::bind(
69 | function () use ($object, $property) {
70 | $return = null;
71 | try {
72 | $class = get_class($object);
73 | if (defined($class . '::' . $property)) {
74 | $return = constant($class . '::' . $property);
75 | } elseif (isset($object::$$property)) {
76 | $return = $object::$$property;
77 | } elseif (isset($object->{$property})) {
78 | $return = $object->{$property};
79 | } else {
80 | throw new Exception(
81 | sprintf(
82 | 'No default, static, or constant property with the name "%s" exists!',
83 | $property
84 | )
85 | );
86 | }
87 | } catch (Exception $error) {
88 | AmqpAgentException::rethrow($error, sprintf('%s::%s() failed!', static::class, __FUNCTION__), false);
89 | }
90 | return $return;
91 | },
92 | null,
93 | $object
94 | )
95 | );
96 | }
97 |
98 | /**
99 | * Sets a private, protected, or public property (default or static) of an object.
100 | * @param object $object Class instance.
101 | * @param string $property Property name.
102 | * @param string $value Property value.
103 | * @return mixed The new property value.
104 | * @throws AmqpAgentException On failure.
105 | */
106 | public static function setProperty($object, string $property, $value)
107 | {
108 | return call_user_func(
109 | Closure::bind(
110 | function () use ($object, $property, $value) {
111 | $return = null;
112 | try {
113 | if (isset($object::$$property)) {
114 | $return = $object::$$property = $value;
115 | } elseif (isset($object->{$property})) {
116 | $return = $object->{$property} = $value;
117 | } else {
118 | throw new Exception(
119 | sprintf(
120 | 'No default or static property with the name "%s" exists!',
121 | $property
122 | )
123 | );
124 | }
125 | } catch (Exception $error) {
126 | AmqpAgentException::rethrow($error, sprintf('%s::%s() failed!', static::class, __FUNCTION__), false);
127 | }
128 | return $return;
129 | },
130 | null,
131 | $object
132 | )
133 | );
134 | }
135 |
136 | /**
137 | * Returns a reflection class instance on a class.
138 | * @param object|string $class Class instance or class FQN.
139 | * @return ReflectionClass
140 | * @throws ReflectionException
141 | */
142 | public static function reflectOnClass($class)
143 | {
144 | return new ReflectionClass($class);
145 | }
146 |
147 | /**
148 | * Returns a reflection object instance on an object.
149 | * @param object $object Class instance.
150 | * @return ReflectionObject
151 | */
152 | public static function reflectOnObject($object)
153 | {
154 | return new ReflectionObject($object);
155 | }
156 |
157 | /**
158 | * Tries to cast an object into a new class. Similar classes work best.
159 | * @param object $fromObject Class instance.
160 | * @param string $toClass Class FQN.
161 | * @return object
162 | * @throws AmqpAgentException When passing a wrong argument or on failure.
163 | */
164 | public static function castObjectToClass($fromObject, string $toClass)
165 | {
166 | if (!is_object($fromObject)) {
167 | throw new AmqpAgentException(
168 | sprintf(
169 | 'The first parameter must be an instance of class, a wrong parameter with (data-type: %s) was passed instead.',
170 | gettype($fromObject)
171 | )
172 | );
173 | }
174 |
175 | if (!class_exists($toClass)) {
176 | throw new AmqpAgentException(
177 | sprintf(
178 | 'Unknown class: %s.',
179 | $toClass
180 | )
181 | );
182 | }
183 |
184 | try {
185 | $toClass = new $toClass();
186 |
187 | $toClassReflection = self::reflectOnObject($toClass);
188 | $fromObjectReflection = self::reflectOnObject($fromObject);
189 |
190 | $fromObjectProperties = $fromObjectReflection->getProperties();
191 |
192 | foreach ($fromObjectProperties as $fromObjectProperty) {
193 | $fromObjectProperty->setAccessible(true);
194 | $name = $fromObjectProperty->getName();
195 | $value = $fromObjectProperty->getValue($fromObject);
196 |
197 | if ($toClassReflection->hasProperty($name)) {
198 | $property = $toClassReflection->getProperty($name);
199 | $property->setAccessible(true);
200 | $property->setValue($toClass, $value);
201 | } else {
202 | try {
203 | self::setProperty($toClass, $name, $value);
204 | } catch (Exception $e) {
205 | // This exception means target object has a __set()
206 | // magic method that prevents setting the property.
207 | }
208 | }
209 | }
210 |
211 | return $toClass;
212 | } catch (Exception $error) {
213 | AmqpAgentException::rethrow($error, sprintf('%s::%s() failed!', static::class, __FUNCTION__), false);
214 | }
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/RPC/AbstractEndpoint.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\RPC;
13 |
14 | use Exception;
15 | use PhpAmqpLib\Connection\AMQPStreamConnection;
16 | use PhpAmqpLib\Channel\AMQPChannel;
17 | use PhpAmqpLib\Message\AMQPMessage;
18 | use MAKS\AmqpAgent\RPC\AbstractEndpointInterface;
19 | use MAKS\AmqpAgent\Helper\EventTrait;
20 | use MAKS\AmqpAgent\Exception\MagicMethodsExceptionsTrait;
21 | use MAKS\AmqpAgent\Exception\RPCEndpointException;
22 | use MAKS\AmqpAgent\Config\RPCEndpointParameters as Parameters;
23 |
24 | /**
25 | * An abstract class implementing the basic functionality of an endpoint.
26 | * @since 2.0.0
27 | * @api
28 | */
29 | abstract class AbstractEndpoint implements AbstractEndpointInterface
30 | {
31 | use MagicMethodsExceptionsTrait;
32 | use EventTrait;
33 |
34 | /**
35 | * The connection options of the RPC endpoint.
36 | * @var array
37 | */
38 | protected $connectionOptions;
39 |
40 | /**
41 | * The queue name of the RPC endpoint.
42 | * @var string
43 | */
44 | protected $queueName;
45 |
46 | /**
47 | * Whether the endpoint is connected to RabbitMQ server or not.
48 | * @var bool
49 | */
50 | protected $connected;
51 |
52 | /**
53 | * The endpoint connection.
54 | * @var AMQPStreamConnection
55 | */
56 | protected $connection;
57 |
58 | /**
59 | * The endpoint channel.
60 | * @var AMQPChannel
61 | */
62 | protected $channel;
63 |
64 | /**
65 | * The request body.
66 | * @var string
67 | */
68 | protected $requestBody;
69 |
70 | /**
71 | * Requests conveyor.
72 | * @var string
73 | */
74 | protected $requestQueue;
75 |
76 | /**
77 | * The response body.
78 | * @var string
79 | */
80 | protected $responseBody;
81 |
82 | /**
83 | * Responses conveyor.
84 | * @var string
85 | */
86 | protected $responseQueue;
87 |
88 | /**
89 | * Correlation ID of the last request/response.
90 | * @var string
91 | */
92 | protected $correlationId;
93 |
94 |
95 | /**
96 | * Class constructor.
97 | * @param array $connectionOptions [optional] The overrides for the default connection options of the RPC endpoint.
98 | * @param string $queueName [optional] The override for the default queue name of the RPC endpoint.
99 | */
100 | public function __construct(?array $connectionOptions = [], ?string $queueName = null)
101 | {
102 | $this->connectionOptions = Parameters::patch($connectionOptions, 'RPC_CONNECTION_OPTIONS');
103 | $this->queueName = empty($queueName) ? Parameters::RPC_QUEUE_NAME : $queueName;
104 | }
105 |
106 | /**
107 | * Closes the connection with RabbitMQ server before destroying the object.
108 | */
109 | public function __destruct()
110 | {
111 | $this->disconnect();
112 | }
113 |
114 |
115 | /**
116 | * Opens a connection with RabbitMQ server.
117 | * @param array|null $connectionOptions [optional] The overrides for the default connection options of the RPC endpoint.
118 | * @return self
119 | * @throws RPCEndpointException If the endpoint is already connected.
120 | */
121 | public function connect(?array $connectionOptions = [])
122 | {
123 | $this->connectionOptions = Parameters::patchWith(
124 | $connectionOptions ?? [],
125 | $this->connectionOptions
126 | );
127 |
128 | if ($this->isConnected()) {
129 | throw new RPCEndpointException('Endpoint is already connected!');
130 | }
131 |
132 | $parameters = array_values($this->connectionOptions);
133 |
134 | $this->connection = new AMQPStreamConnection(...$parameters);
135 | $this->trigger('connection.after.open', [$this->connection]);
136 |
137 | $this->channel = $this->connection->channel();
138 | $this->trigger('channel.after.open', [$this->channel]);
139 |
140 | return $this;
141 | }
142 |
143 | /**
144 | * Closes the connection with RabbitMQ server.
145 | * @return void
146 | */
147 | public function disconnect(): void
148 | {
149 | if ($this->isConnected()) {
150 | $this->connected = null;
151 |
152 | $this->trigger('channel.before.close', [$this->channel]);
153 | $this->channel->close();
154 |
155 | $this->trigger('connection.before.close', [$this->connection]);
156 | $this->connection->close();
157 | }
158 | }
159 |
160 | /**
161 | * Returns whether the endpoint is connected or not.
162 | * @return bool
163 | */
164 | public function isConnected(): bool
165 | {
166 | $this->connected = (
167 | isset($this->connection) &&
168 | isset($this->channel) &&
169 | $this->connection->isConnected() &&
170 | $this->channel->is_open()
171 | );
172 |
173 | return $this->connected;
174 | }
175 |
176 | /**
177 | * Returns the connection used by the endpoint.
178 | * @return AMQPStreamConnection
179 | */
180 | public function getConnection(): AMQPStreamConnection
181 | {
182 | return $this->connection;
183 | }
184 |
185 | /**
186 | * The time needed for the round-trip to RabbitMQ server in milliseconds.
187 | * Note that if the endpoint is not connected yet, this method will establish a new connection only for checking.
188 | * @return float A two decimal points rounded float.
189 | */
190 | final public function ping(): float
191 | {
192 | try {
193 | $pingConnection = $this->connection;
194 | if (!isset($pingConnection) || !$pingConnection->isConnected()) {
195 | $parameters = array_values($this->connectionOptions);
196 | $pingConnection = new AMQPStreamConnection(...$parameters);
197 | }
198 | $pingChannel = $pingConnection->channel();
199 |
200 | [$pingQueue] = $pingChannel->queue_declare(
201 | null,
202 | false,
203 | false,
204 | true,
205 | true
206 | );
207 |
208 | $pingChannel->basic_qos(
209 | null,
210 | 1,
211 | null
212 | );
213 |
214 | $pingEcho = null;
215 |
216 | $pingChannel->basic_consume(
217 | $pingQueue,
218 | null,
219 | false,
220 | false,
221 | false,
222 | false,
223 | function ($message) use (&$pingEcho) {
224 | $message->ack();
225 | $pingEcho = $message->body;
226 | }
227 | );
228 |
229 | $pingStartTime = microtime(true);
230 |
231 | $pingChannel->basic_publish(
232 | new AMQPMessage(__FUNCTION__),
233 | null,
234 | $pingQueue
235 | );
236 |
237 | while (!$pingEcho) {
238 | $pingChannel->wait();
239 | }
240 |
241 | $pingEndTime = microtime(true);
242 |
243 | $pingChannel->queue_delete($pingQueue);
244 |
245 | if ($pingConnection === $this->connection) {
246 | $pingChannel->close();
247 | } else {
248 | $pingChannel->close();
249 | $pingConnection->close();
250 | }
251 |
252 | return round(($pingEndTime - $pingStartTime) * 1000, 2);
253 | } catch (Exception $error) {
254 | RPCEndpointException::rethrow($error);
255 | }
256 | }
257 |
258 | /**
259 | * Hooking method based on events to manipulate the request/response during the endpoint/message life cycle.
260 | * Check out `self::$events` via `self::getEvents()` after processing at least one request/response to see all available events.
261 | *
262 | * The parameters will be passed to the callback as follows:
263 | * 1. `$listenedOnObject` (first segment of event name e.g. `'connection.after.open'` will be `$connection`),
264 | * 2. `$calledOnObject` (the object this method was called on e.g. `$endpoint`),
265 | * 3. `$eventName` (the event was listened on e.g. `'connection.after.open'`).
266 | * ```
267 | * $endpoint->on('connection.after.open', function ($connection, $endpoint, $event) {
268 | * ...
269 | * });
270 | * ```
271 | * @param string $event The event to listen on.
272 | * @param callable $callback The callback to execute.
273 | * @return self
274 | */
275 | final public function on(string $event, callable $callback)
276 | {
277 | $this->bind($event, function (...$arguments) use ($event, $callback) {
278 | call_user_func_array(
279 | $callback,
280 | array_merge(
281 | $arguments,
282 | [$this, $event]
283 | )
284 | );
285 | });
286 |
287 | return $this;
288 | }
289 |
290 | /**
291 | * Hook method to manipulate the message (request/response) when extending the class.
292 | * @param AMQPMessage $message
293 | * @return string
294 | */
295 | abstract protected function callback(AMQPMessage $message): string;
296 | }
297 |
--------------------------------------------------------------------------------
/src/Helper/Utility.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use Exception;
15 | use DateTime;
16 | use DateTimeZone;
17 |
18 | /**
19 | * A class containing miscellaneous helper functions.
20 | * @since 1.2.0
21 | */
22 | final class Utility
23 | {
24 | /**
25 | * Returns a DateTime object with the right time zone.
26 | * @param string $time A valid php date/time string.
27 | * @param string|null $timezone A valid php timezone string.
28 | * @return DateTime
29 | * @throws Exception
30 | */
31 | public static function time(string $time = 'now', ?string $timezone = null): DateTime
32 | {
33 | $timezone = $timezone
34 | ? $timezone
35 | : date_default_timezone_get();
36 |
37 | $timezoneObject = $timezone
38 | ? new DateTimeZone($timezone)
39 | : null;
40 |
41 | return new DateTime($time, $timezoneObject);
42 | }
43 |
44 | /**
45 | * Generates a user-level notice, warning, or an error with styling.
46 | * @param array|string|null $text [optional] The text wished to be styled (when passing an array, if array key is a valid color it will style this array element value with its key).
47 | * @param string $color [optional] Case sensitive ANSI color name in this list [black, red, green, yellow, magenta, cyan, white, default] (when passing array, this parameter will be the fallback).
48 | * @param int $type [optional] Error type (E_USER family). 1024 E_USER_NOTICE, 512 E_USER_WARNING, 256 E_USER_ERROR, 16384 E_USER_DEPRECATED.
49 | * @return bool True if error type is accepted.
50 | */
51 | public static function emit($text = null, ?string $color = 'yellow', int $type = E_USER_NOTICE): bool
52 | {
53 | $colors = [
54 | 'reset' => 0,
55 | 'black' => 30,
56 | 'red' => 31,
57 | 'green' => 32,
58 | 'yellow' => 33,
59 | 'blue' => 34,
60 | 'magenta' => 35,
61 | 'cyan' => 36,
62 | 'white' => 37,
63 | 'default' => 39,
64 | ];
65 |
66 | $types = [
67 | E_USER_NOTICE => E_USER_NOTICE,
68 | E_USER_WARNING => E_USER_WARNING,
69 | E_USER_ERROR => E_USER_ERROR,
70 | E_USER_DEPRECATED => E_USER_DEPRECATED,
71 | ];
72 |
73 | $cli = php_sapi_name() === 'cli' || php_sapi_name() === 'cli-server' || http_response_code() === false;
74 |
75 | $trim = ' \t\0\x0B';
76 | $backspace = chr(8);
77 | $wrapper = $cli ? "\033[%dm %s\033[0m" : "@COLOR[%d] %s";
78 | $color = $colors[$color] ?? 39;
79 | $type = $types[$type] ?? 1024;
80 | $message = '';
81 |
82 | if (is_array($text)) {
83 | foreach ($text as $segmentColor => $string) {
84 | $string = trim($string, $trim);
85 | if (is_string($segmentColor)) {
86 | $segmentColor = $colors[$segmentColor] ?? $color;
87 | $message .= !strlen($message)
88 | ? sprintf($wrapper, $segmentColor, $backspace . $string)
89 | : sprintf($wrapper, $segmentColor, $string);
90 | continue;
91 | }
92 | $message = $message . $string;
93 | }
94 | } elseif (is_string($text)) {
95 | $string = $backspace . trim($text, $trim);
96 | $message = sprintf($wrapper, $color, $string);
97 | } else {
98 | $string = $backspace . 'From ' . __METHOD__ . ': No message was specified!';
99 | $message = sprintf($wrapper, $color, $string);
100 | }
101 |
102 | $message = $cli ? $message : preg_replace('/@COLOR\[\d+\]/', '', $message);
103 |
104 | return trigger_error($message, $type);
105 | }
106 |
107 | /**
108 | * Returns the passed key(s) from the backtrace. Note that the backtrace is reversed (last is first).
109 | * @param string|array $pluck The key to to get as a string or an array of strings (keys) from this list [file, line, function, class, type, args].
110 | * @param int $offset [optional] The offset of the backtrace (last executed is index at 0).
111 | * @return string|int|array|null A string or int if a string is passed, an array if an array is passed and null if no match was found.
112 | */
113 | public static function backtrace($pluck, int $offset = 0)
114 | {
115 | $backtrace = array_reverse(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT));
116 | $plucked = null;
117 |
118 | if (count($backtrace) < $offset + 1) {
119 | return null;
120 | } elseif (is_string($pluck)) {
121 | $plucked = isset($backtrace[$offset][$pluck]) ? $backtrace[$offset][$pluck] : null;
122 | } elseif (is_array($pluck)) {
123 | $plucked = [];
124 | foreach ($pluck as $key) {
125 | !isset($backtrace[$offset][$key]) ?: $plucked[$key] = $backtrace[$offset][$key];
126 | }
127 | }
128 |
129 | return is_string($plucked) || is_array($plucked) && count($plucked, COUNT_RECURSIVE) ? $plucked : null;
130 | }
131 |
132 | /**
133 | * Executes a CLI command in the specified path synchronously or asynchronous (cross platform).
134 | * @since 2.0.0
135 | * @param string $command The command to execute.
136 | * @param string|null $path [optional] The path where the command should be executed.
137 | * @param bool $asynchronous [optional] Whether the command should be a background process (asynchronous) or not (synchronous).
138 | * @return string|null The command result (as a string if possible) if synchronous otherwise null.
139 | * @throws Exception
140 | */
141 | public static function execute(string $command, string $path = null, bool $asynchronous = false): ?string
142 | {
143 | if (!strlen($command)) {
144 | throw new Exception('No valid command is specified!');
145 | }
146 |
147 | $isWindows = PHP_OS == 'WINNT' || substr(php_uname(), 0, 7) == 'Windows';
148 | $apWrapper = $isWindows ? 'start /B %s > NUL' : '/usr/bin/nohup %s >/dev/null 2>&1 &';
149 |
150 | if ($path && strlen($path) && getcwd() !== $path) {
151 | chdir(realpath($path));
152 | }
153 |
154 | if ($asynchronous) {
155 | $command = sprintf($apWrapper, $command);
156 | }
157 |
158 | if ($isWindows && $asynchronous) {
159 | pclose(popen($command, 'r'));
160 | return null;
161 | }
162 |
163 | return shell_exec($command);
164 | }
165 |
166 | /**
167 | * Returns an HTTP Response to the browser and lets blocking code that comes after this function to continue executing in the background.
168 | * This function is useful for example in Controllers that do some long-running tasks, and you just want to inform the client that the job has been started.
169 | * Please note that this function is tested MANUALLY ONLY, it is provided as is, there is no guarantee that it will work as expected nor on all platforms.
170 | * @param string $body The response body.
171 | * @param int $status [optional] The response status code.
172 | * @param array $headers [optional] An associative array of additional response headers `['headerName' => 'headerValue']`.
173 | * @return void
174 | * @since 2.2.0
175 | * @codeCoverageIgnore
176 | */
177 | public static function respond(string $body, int $status = 200, array $headers = []): void
178 | {
179 | // client disconnection should not abort script execution
180 | ignore_user_abort(true);
181 | // script execution should not bound by a timeout
182 | set_time_limit(0);
183 |
184 | // writing to the session must be closed to prevents subsequent requests from hanging
185 | if (session_id()) {
186 | session_write_close();
187 | }
188 |
189 | // clean the output buffer and turn off output buffering
190 | ob_end_clean();
191 | // turn on output buffering and buffer all upcoming output
192 | ob_start();
193 |
194 | // echo out response body (message)
195 | echo($body);
196 |
197 | // only keep the last buffer if nested and get the length of the output buffer
198 | while (ob_get_level() > 1) {
199 | ob_end_flush();
200 | }
201 | $length = ob_get_level() ? ob_get_length() : 0;
202 |
203 | // reserved headers that must not be overwritten
204 | $reservedHeaders = [
205 | 'Connection' => 'close',
206 | 'Content-Encoding' => 'none',
207 | 'Content-Length' => $length
208 | ];
209 |
210 | // user headers after filtering out the reserved headers
211 | $filteredHeaders = array_filter($headers, function ($key) use ($reservedHeaders) {
212 | $immutable = array_map('strtolower', array_keys($reservedHeaders));
213 | $mutable = strtolower($key);
214 | return !in_array($mutable, $immutable);
215 | }, ARRAY_FILTER_USE_KEY);
216 |
217 | // final headers for the response
218 | $finalHeaders = array_merge($reservedHeaders, $filteredHeaders);
219 |
220 | // send headers to tell the browser to close the connection
221 | foreach ($finalHeaders as $headerName => $headerValue) {
222 | header(sprintf('%s: %s', $headerName, $headerValue));
223 | }
224 |
225 | // set the HTTP response code
226 | http_response_code($status);
227 |
228 | // flush the output buffer and turn off output buffering
229 | ob_end_flush();
230 |
231 | // flush all output buffer layers
232 | if (ob_get_level()) {
233 | ob_flush();
234 | }
235 |
236 | // flush system output buffer
237 | flush();
238 |
239 | echo('You should not be seeing this!');
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/Helper/Serializer.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Helper;
13 |
14 | use Exception;
15 | use Closure;
16 | use MAKS\AmqpAgent\Exception\SerializerViolationException;
17 |
18 | /**
19 | * A flexible serializer to be used in conjunction with the workers.
20 | * @since 1.0.0
21 | */
22 | class Serializer
23 | {
24 | /**
25 | * The JSON serialization type constant.
26 | * @var string
27 | */
28 | public const TYPE_JSON = 'JSON';
29 |
30 | /**
31 | * The PHP serialization type constant.
32 | * @var string
33 | */
34 | public const TYPE_PHP = 'PHP';
35 |
36 | /**
37 | * The default data the serializer works with if none was provided.
38 | * @var null
39 | */
40 | public const DEFAULT_DATA = null;
41 |
42 | /**
43 | * The default type the serializer works with if none was provided.
44 | * @var string
45 | */
46 | public const DEFAULT_TYPE = self::TYPE_JSON;
47 |
48 | /**
49 | * The default strict value the serializer works with if none was provided.
50 | * @var bool
51 | */
52 | public const DEFAULT_STRICT = true;
53 |
54 | /**
55 | * The supported serialization types.
56 | * @var array
57 | */
58 | protected const SUPPORTED_TYPES = [self::TYPE_JSON, self::TYPE_PHP];
59 |
60 |
61 | /**
62 | * The current data the serializer has.
63 | * @var mixed
64 | */
65 | protected $data;
66 |
67 | /**
68 | * The current type the serializer uses.
69 | * @var string
70 | */
71 | protected $type;
72 |
73 | /**
74 | * The current strict value the serializer works with.
75 | * @var bool
76 | */
77 | protected $strict;
78 |
79 | /**
80 | * The result of the last (un)serialization operation.
81 | * @var mixed
82 | */
83 | protected $result;
84 |
85 |
86 | /**
87 | * Serializer object constructor.
88 | * @param mixed $data [optional] The data to (un)serialize. Defaults to null.
89 | * @param string|null $type [optional] The type of (un)serialization. Defaults to JSON.
90 | * @param bool $strict [optional] Whether or not to assert that no errors have occurred while executing (un)serialization functions. Defaults to true.
91 | * @throws SerializerViolationException
92 | */
93 | public function __construct($data = null, ?string $type = null, ?bool $strict = null)
94 | {
95 | $this->setData($data ?? self::DEFAULT_DATA);
96 | $this->setType($type ?? self::DEFAULT_TYPE);
97 | $this->setStrict($strict ?? self::DEFAULT_STRICT);
98 | }
99 |
100 | /**
101 | * Executes when calling the class like a function.
102 | * @param mixed $data The data to (un)serialize.
103 | * @param string|null $type [optional] The type of (un)serialization. Defaults to JSON.
104 | * @param bool $strict [optional] Whether or not to assert that no errors have occurred while executing (un)serialization functions. Defaults to true.
105 | * @return mixed Serialized or unserialized data depending on the passed parameters.
106 | * @throws SerializerViolationException
107 | */
108 | public function __invoke($data, ?string $type = self::DEFAULT_TYPE, ?bool $strict = self::DEFAULT_STRICT)
109 | {
110 | $this->setData($data);
111 | $this->setType($type ?? self::DEFAULT_TYPE);
112 | $this->setStrict($strict ?? self::DEFAULT_STRICT);
113 |
114 | try {
115 | $this->result = is_string($data) ? $this->unserialize() : $this->serialize();
116 | } catch (Exception $error) {
117 | $dataType = gettype($data);
118 | throw new SerializerViolationException(
119 | sprintf(
120 | 'The data passed to the serializer (data-type: %s) could not be processed!',
121 | $dataType
122 | ),
123 | (int)$error->getCode(),
124 | $error
125 | );
126 | }
127 |
128 | return $this->result;
129 | }
130 |
131 |
132 | /**
133 | * Serializes the passed or registered data. When no parameters are passed, it uses the registered ones.
134 | * @param mixed $data [optional] The data to serialize.
135 | * @param string|null $type [optional] The type of serialization.
136 | * @param bool $strict [optional] Whether or not to assert that no errors have occurred while executing serialization functions.
137 | * @return string|null A serialized representation of the passed or registered data or null on failure.
138 | * @throws SerializerViolationException
139 | */
140 | public function serialize($data = null, ?string $type = null, ?bool $strict = null): string
141 | {
142 | if (null !== $data) {
143 | $this->setData($data);
144 | }
145 |
146 | if (null !== $type) {
147 | $this->setType($type);
148 | }
149 |
150 | if (null !== $strict) {
151 | $this->setStrict($strict);
152 | }
153 |
154 | if (self::TYPE_PHP === $this->type) {
155 | $this->assertNoPhpSerializationError(function () {
156 | $this->result = serialize($this->data);
157 | });
158 | }
159 |
160 | if (self::TYPE_JSON === $this->type) {
161 | $this->assertNoJsonSerializationError(function () {
162 | $this->result = json_encode($this->data);
163 | });
164 | }
165 |
166 | return $this->result;
167 | }
168 |
169 | /**
170 | * Unserializes the passed or registered data. When no parameters are passed, it uses the registered ones.
171 | * @param string|null $data [optional] The data to unserialize.
172 | * @param string|null $type [optional] The type of unserialization.
173 | * @param bool $strict [optional] Whether or not to assert that no errors have occurred while executing unserialization functions.
174 | * @return mixed A PHP type on success or false or null on failure.
175 | * @throws SerializerViolationException
176 | */
177 | public function unserialize(?string $data = null, ?string $type = null, ?bool $strict = null)
178 | {
179 | if (null !== $data) {
180 | $this->setData($data);
181 | }
182 |
183 | if (null !== $type) {
184 | $this->setType($type);
185 | }
186 |
187 | if (null !== $strict) {
188 | $this->setStrict($strict);
189 | }
190 |
191 | if (self::TYPE_PHP === $this->type) {
192 | $this->assertNoPhpSerializationError(function () {
193 | $this->result = unserialize($this->data);
194 | });
195 | }
196 |
197 | if (self::TYPE_JSON === $this->type) {
198 | $this->assertNoJsonSerializationError(function () {
199 | $this->result = json_decode($this->data, true);
200 | });
201 | }
202 |
203 | return $this->result;
204 | }
205 |
206 | /**
207 | * Deserializes the passed or registered data. When no parameters are passed, it uses the registered ones.
208 | * @since 1.2.2 Alias for `self::unserialize()`.
209 | * @param string|null $data [optional] The data to unserialize.
210 | * @param string|null $type [optional] The type of unserialization.
211 | * @param bool $strict [optional] Whether or not to assert that no errors have occurred while executing unserialization functions.
212 | * @return mixed A PHP type on success or false or null on failure.
213 | * @throws SerializerViolationException
214 | */
215 | public function deserialize(?string $data = null, ?string $type = null, ?bool $strict = null)
216 | {
217 | return $this->unserialize($data, $type, $strict);
218 | }
219 |
220 | /**
221 | * Registers the passed data in the object.
222 | * @param mixed $data The data wished to be registered.
223 | * @return self
224 | */
225 | public function setData($data)
226 | {
227 | $this->data = $data;
228 |
229 | return $this;
230 | }
231 |
232 | /**
233 | * Returns the currently registered data.
234 | * @return mixed
235 | */
236 | public function getData()
237 | {
238 | return $this->data;
239 | }
240 |
241 | /**
242 | * Registers the passed type in the object.
243 | * @param string $type The type wished to be registered.
244 | * @return self
245 | * @throws SerializerViolationException
246 | */
247 | public function setType(string $type)
248 | {
249 | $type = strtoupper($type);
250 |
251 | if (!in_array($type, static::SUPPORTED_TYPES)) {
252 | throw new SerializerViolationException(
253 | sprintf(
254 | '"%s" is unsupported (un)serialization type. Supported types are: [%s]!',
255 | $type,
256 | implode(', ', static::SUPPORTED_TYPES)
257 | )
258 | );
259 | }
260 |
261 | $this->type = $type;
262 |
263 | return $this;
264 | }
265 |
266 | /**
267 | * Returns the currently registered type.
268 | * @return string
269 | */
270 | public function getType(): string
271 | {
272 | return $this->type;
273 | }
274 |
275 | /**
276 | * Registers the passed strict value in the object.
277 | * @since 1.2.2
278 | * @param bool $strict The strict value wished to be registered.
279 | * @return self
280 | */
281 | public function setStrict(bool $strict)
282 | {
283 | $this->strict = $strict;
284 |
285 | return $this;
286 | }
287 |
288 | /**
289 | * Returns the currently registered strict value.
290 | * @since 1.2.2
291 | * @return bool
292 | */
293 | public function isStrict()
294 | {
295 | return $this->strict;
296 | }
297 |
298 | /**
299 | * Alias for `self::serialize()` that does not accept any parameters (works with currently registered parameters).
300 | * @return string The serialized data.
301 | * @throws SerializerViolationException
302 | */
303 | public function getSerialized(): string
304 | {
305 | return $this->serialize();
306 | }
307 |
308 | /**
309 | * Alias for `self::unserialize()` that does not accept any parameters (works with currently registered parameters).
310 | * @return mixed The unserialized data.
311 | * @throws SerializerViolationException
312 | */
313 | public function getUnserialized()
314 | {
315 | return $this->unserialize();
316 | }
317 |
318 | /**
319 | * Asserts that `serialize()` and/or `unserialize()` was executed successfully depending on strictness of the Serializer.
320 | * @since 1.2.2
321 | * @param Closure $callback The (un)serialization callback to execute.
322 | * @return void
323 | * @throws SerializerViolationException
324 | */
325 | protected function assertNoPhpSerializationError(Closure $callback): void
326 | {
327 | $this->result = null;
328 |
329 | try {
330 | $callback();
331 | } catch (Exception $error) {
332 | if ($this->strict) {
333 | throw new SerializerViolationException(
334 | sprintf(
335 | 'An error occurred while executing serialize() or unserialize(): %s',
336 | (string)$error->getMessage()
337 | ),
338 | (int)$error->getCode(),
339 | $error
340 | );
341 | }
342 | }
343 | }
344 |
345 | /**
346 | * Asserts that `json_encode()` and/or `json_decode()` was executed successfully depending on strictness of the Serializer.
347 | * @since 1.2.2
348 | * @param Closure $callback The (un)serialization callback to execute.
349 | * @return void
350 | * @throws SerializerViolationException
351 | */
352 | protected function assertNoJsonSerializationError(Closure $callback): void
353 | {
354 | $this->result = null;
355 |
356 | try {
357 | $callback();
358 | } catch (Exception $error) {
359 | // JSON functions do not throw exceptions on PHP < v7.3.0
360 | // The code down below takes care of throwing the exception.
361 | }
362 |
363 | if ($this->strict) {
364 | $errorCode = json_last_error();
365 | if ($errorCode !== JSON_ERROR_NONE) {
366 | $errorMessage = json_last_error_msg();
367 | throw new SerializerViolationException(
368 | sprintf(
369 | 'An error occurred while executing json_encode() or json_decode(): %s',
370 | $errorMessage
371 | )
372 | );
373 | }
374 | }
375 | }
376 | }
377 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to **AMQP Agent** will be documented in this file.
4 |
5 |
6 | ## [Unreleased]
7 |
8 |
9 |
10 |
11 | ## [[1.0.0] - 2020-06-15](https://github.com/MarwanAlsoltany/amqp-agent/commits/v1.0.0)
12 | - Initial release.
13 |
14 |
15 |
16 |
17 | ## [[1.0.1] - 2020-06-23](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.0.0...v1.0.1)
18 | - Fix issue with Logger class:
19 | - Fix additional line breaks when writing to log file.
20 |
21 |
22 |
23 |
24 | ## [[1.1.0] - 2020-08-10](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.0.1...v1.1.0)
25 | - Add the possibility to open multiple connection by a worker.
26 | - Update `AbstractWorker` class:
27 | - Add connections array and channels array.
28 | - Add `setConnection()`, `getNewConnection()`, and `setChannel()` methods.
29 | - Modify old methods to make use of the newly created methods internally.
30 | - Add new tests to the newly created methods.
31 | - Update methods signature on the corresponding interface (`AbstractWorkerInterface`)
32 | - Update DocBlocks of other classes to reference the newly created methods.
33 | - Rebuild documentation.
34 |
35 |
36 |
37 |
38 | ## [[1.1.1] - 2020-09-14](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.0.1...v1.1.1)
39 | - Update `composer.json`:
40 | - Bump minimum **php-amqplib** version.
41 | - Downgrade minimum php version.
42 | - Update dev requirements versions to match php version.
43 | - Update branch-alias.
44 | - Update scripts field.
45 | - Add conflict field.
46 | - Fix **php < 7.4** type hinting incompatibility:
47 | - Remove return type "self" from methods signature in all interfaces.
48 | - Fix **php-amqplib** v2.12.0 deprecations:
49 | - Fix references to deprecated properties in **php-amqplib** v2.12.0.
50 | - Change `AbstractWorker` arguments method to a static method.
51 | - Refactor some internal functionalities in different classes.
52 | - Update tests so that they run faster.
53 | - Update Travis config.
54 | - Rebuild documentation.
55 |
56 |
57 |
58 |
59 | ## [[1.2.0] - 2020-09-26](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.1.1...v1.2.0)
60 | - Update `composer.json`:
61 | - Add a link for the documentation.
62 | - Add some suggestions.
63 | - Update `dev-autoload` namespace.
64 | - Fix typos and update DocBlocks:
65 | - Fix some typos in DocBlocks and other parts of the codebase.
66 | - Add examples to major classes DocBlocks.
67 | - Add `Utility` class to contain some miscellaneous reusable functions.
68 | - Refactor `Logger` class:
69 | - Internal changes to make use of the `Utility` class.
70 | - Better writing directory guessing when no path is specified.
71 | - Update `AmqpAgentException` class:
72 | - Fix issue of wrong fully-qualified name when casting the class to a string.
73 | - Change default message of `rethrowException` method to a more useful one.
74 | - Add a new parameter to change the wrapping thrown exception class.
75 | - Rename the method `rethrowException()` to `rethrow()` and add the old name as an alias.
76 | - Add `MagicMethodsExceptionsTrait` to unify error messages of calls to magic methods.
77 | - Add `AbstractParameters` class to simplify working with parameters:
78 | - Add `AmqpAgentParameters` as global class for all parameters.
79 | - Add worker specific parameters class (`AbstractWorkerParameters`, `PublisherParameters`, `ConsumerParameters`).
80 | - Update configuration file (`maks-amqp-agent-config.php`) to make use of the newly created `AmqpAgentParameters` class.
81 | - Refactor workers classes (`AbstractWorker`, `Publisher`, `Consumer`):
82 | - Make use of the newly created `*Parameters` class.
83 | - Make use of the newly created `MagicMethodsExceptionsTrait`.
84 | - Remove `@codeCoverageIgnore` annotations from workers classes.
85 | - Remove constants from the corresponding `*Interface` as they are available now via `*Parameters`.
86 | - Update the classes in different places to make use of the new additions.
87 | - Update `WorkerCommandTrait` to make use of the newly created `AmqpAgentParameters` class.
88 | - Remove protected method `mutateClassConst()` from `WorkerMutationTrait` as it is not used anymore (usage replaced with `*Parameters::patchWith()`).
89 | - Update old tests to cover the new changes.
90 | - Update tests
91 | - Add new tests for the newly created classes and functions.
92 | - Update `phpunit.xml.dist` to run the new tests.
93 | - Update namespace across all test classes.
94 | - Remove `*Mock` classes from `*Test` classes and move them to their own namespace.
95 | - Rebuild documentation.
96 | - Update formatting of `CHANGELOG.md`.
97 |
98 |
99 |
100 |
101 | ## [[1.2.1] - 2020-09-30](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.2.0...v1.2.1)
102 | - Update `composer.json`:
103 | - Update `branch-alias` version.
104 | - Update `Utility` class:
105 | - Add `collapse()` method.
106 | - Update `Client` class:
107 | - Add `gettable()` method.
108 | - Refactor `get()` method.
109 | - Refactor `Logger` class:
110 | - Add `getFallbackFilename()` method.
111 | - Add `getFallbackDirectory()` method.
112 | - Add `getNormalizedPath()` method.
113 | - Refactor `log()` method to make use of the newly created methods.
114 | - Update `MagicMethodsExceptionsTrait`:
115 | - Update exceptions messages to prevent notices when passing an array as an argument to magic methods.
116 | - Fix coding style issues in different places.
117 | - Rebuild documentation.
118 |
119 |
120 |
121 |
122 | ## [[1.2.2] - 2020-11-29](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.2.1...v1.2.2)
123 | - Update `Config` class:
124 | - Remove deprecated method `get()`.
125 | - Remove `$configFlat` property and all of its references.
126 | - Update `$configPath` property to be a realpath.
127 | - Add `has()` method to quickly check if a config value exists.
128 | - Add `get()` method to quickly get a config value (new functionality, should be backwards compatible).
129 | - Add `set()` method to quickly set a config value.
130 | - Update `Serializer` class:
131 | - Add serializations types as class constants.
132 | - Add methods to assert for PHP und JSON (un)serializations errors.
133 | - Refactor `serialize()` and `unserialize()` methods to use assertions.
134 | - Refactor `setType()` method to check if the type is supported.
135 | - Add a new `$strict` property to determine serialization strictness and its corresponding `getStrict()` and `setStrict()` methods.
136 | - Add `$strict` parameter to `serialize()` and `unserialize()` methods.
137 | - Add `deserialize()` method as an alias for `unserialize()`.
138 | - Refactor different methods to make use of available setters.
139 | - Update DocBlocks und Exceptions Messages of different methods.
140 | - Update `Utility` class:
141 | - Add `objectToArray()` method.
142 | - Add `arrayToObject()` method.
143 | - Add `getArrayValueByKey()` method.
144 | - Add `setArrayValueByKey()` method.
145 | - Update `Logger` class:
146 | - Fix an issue with `log()` method when checking for file size.
147 | - Update tests
148 | - Add new tests to the newly created methods.
149 | - Update old tests to cover the new changes.
150 | - Fix coding style issues in different places.
151 | - Rebuild documentation.
152 |
153 |
154 |
155 |
156 | ## [[2.0.0] - 2020-12-03](https://github.com/MarwanAlsoltany/amqp-agent/compare/v1.2.2...v2.0.0)
157 | - Update `composer.json`:
158 | - Update `branch-alias` version.
159 | - Add RPC endpoints interfaces:
160 | - Add `AbstractEndpointInterface`.
161 | - Add `ClientEndpointInterface`.
162 | - Add `ServerEndpointInterface`.
163 | - Add RPC endpoints classes:
164 | - Add `AbstractEndpoint` class.
165 | - Add `ClientEndpoint` class.
166 | - Add `ServerEndpoint` class.
167 | - Add `RPCEndpointParameters` class.
168 | - Add `RPCEndpointException` class.
169 | - Add `IDGenerator` class for generating unique IDs and Tokens.
170 | - Add `EventTrait` and its corresponding `Event` class to expose a simplified API for handling events.
171 | - Add `ClassProxyTrait` and its corresponding `ClassProxy` class to expose a simplified API for manipulating objects.
172 | - Add `ArrayProxyTrait` and its corresponding `ArrayProxy` class to expose a simplified API for manipulating arrays.
173 | - Update `Utility` class:
174 | - Add `execute()` method.
175 | - Remove `collapse()` method (extracted to `ArrayProxy`).
176 | - Remove `objectToArray()` method (extracted to `ArrayProxy`).
177 | - Remove `arrayToObject()` method (extracted to `ArrayProxy`).
178 | - Remove `getArrayValueByKey()` method (extracted to `ArrayProxy`).
179 | - Remove `setArrayValueByKey()` method (extracted to `ArrayProxy`).
180 | - Update `Client` class:
181 | - Add `$clientEndpoint` property.
182 | - Add `$serverEndpoint` property.
183 | - Add `getClientEndpoint()` method.
184 | - Add `getServerEndpoint()` method.
185 | - Update `AmqpAgentParameters` class:
186 | - Add parameters for RPC endpoints.
187 | - Update `Config` class:
188 | - Add references to RPC endpoints properties (`$rpcConnectionOptions` and `$rpcQueueName`).
189 | - Update configuration file (`maks-amqp-agent-config.php`):
190 | - Add references to RPC endpoints options.
191 | - Update tests
192 | - Add new tests to the newly created methods and classes.
193 | - Add new mocks to help with classes testing.
194 | - Add `bin/endpoint` executable to help with endpoints testing.
195 | - Update old tests to cover the new changes.
196 | - Update `phpunit.xml.dist` to run the new tests.
197 | - Rebuild documentation.
198 |
199 |
200 |
201 |
202 | ## [[2.1.0] - 2021-01-12](https://github.com/MarwanAlsoltany/amqp-agent/compare/v2.0.0...v2.1.0)
203 | - Update `composer.json`:
204 | - Update `branch-alias` version.
205 | - Update `scripts` field.
206 | - Update `AbstractWorker` class
207 | - Remove some useless code.
208 | - Change exception type of `shutdown()` method to `AmqpAgentException`.
209 | - Remove return value type hint `self` from methods signature due to unexpected behavior with different PHP versions.
210 | - Update `Publisher` class
211 | - Remove some useless code.
212 | - Change exception type of `publish()` and `publishBatch()` methods to `AmqpAgentException`.
213 | - Remove return value type hint `self` from methods signature due to unexpected behavior with different PHP versions.
214 | - Update `Consumer` class
215 | - Change `nack()` method wrong signature (remove default value from first parameter as it's useless).
216 | - Update method signature on the corresponding interface (`ConsumerInterface`).
217 | - Remove return value type hint `self` from methods signature due to unexpected behavior with different PHP versions.
218 | - Update `WorkerCommandTrait`
219 | - Remove some useless code.
220 | - Update `WorkerMutationTrait`
221 | - Change signature of `mutateClass()` method (remove default value from second parameter as it's useless).
222 | - Change visibility of `mutateClass()` from protected to private (this method should never be used directly).
223 | - Update `Config` class
224 | - Add additional check for return value of `realpath()` function in class constructor.
225 | - Remove return value type hint `self` from methods signature due to unexpected behavior with different PHP versions.
226 | - Update `Utility` class
227 | - Add additional check for script execution path in `execute()` method.
228 | - Update `Logger` class
229 | - Fix a wrong parameter type passed to third argument of `file_put_contents()` function.
230 | - Update `Serializer` class
231 | - Remove return value type hint `self` from methods signature due to unexpected behavior with different PHP versions.
232 | - Update `ClassProxyTrait`
233 | - Add additional check for `$fromObject` parameter of the `castObjectToClass()` method.
234 | - Add a third parameter to the call of `AmqpAgentException::rethrow()` method.
235 | - Update `AmqpAgentException`
236 | - Change default value of `$wrap` parameter from `false` to `true` (this is the expected behavior according to parameter's description).
237 | - Update tests to cover the new minor changes.
238 | - Add `declare(strict_types=1)` to all files of the package.
239 | - Update coding style to PSR12.
240 | - Fix coding style issues in all files of the package.
241 | - Fix DocBlocks in all files of the package.
242 | - Fix typos in all files of the package.
243 | - Update Continuous Integration config files.
244 | - Update Development Dependencies config files.
245 | - Rebuild documentation.
246 |
247 |
248 |
249 |
250 | ## [[2.2.0] - 2022-05-12](https://github.com/MarwanAlsoltany/amqp-agent/compare/v2.1.0...v2.2.0)
251 |
252 | - Update `composer.json`:
253 | - Bump minimum **php-amqplib** version.
254 | - Update **php** requirement.
255 | - Update branch-alias.
256 | - Update `WorkerFacilitationInterface`:
257 | - Change `work()` method return value type hint (from `bool` to `void`).
258 | - Update `Publisher` class:
259 | - Update `publishBatch()` method, it takes now `$parameters` (array) instead of `$_exchange` (string) just like the `publish()` method.
260 | - Update `publishBatch()` method signature on the corresponding interface (`PublisherInterface`).
261 | - Update `work()` method implementation to cover the new changes introduced to the `WorkerFacilitationInterface`.
262 | - Update DocBlock on the corresponding `PublisherSingleton` class of the affected methods.
263 | - Update `Consumer` class:
264 | - Update `work()` method implementation to cover the new changes introduced to the `WorkerFacilitationInterface`.
265 | - Update `consume()` method to shut down all opened channels and connections.
266 | - Update DocBlock on the corresponding `ConsumerSingleton` class of the affected methods.
267 | - Update `Utility` class:
268 | - Add `respond()` method.
269 | - Update tests to cover the new changes.
270 | - Fix some typos in DocBlocks and other parts of the codebase.
271 | - Rebuild documentation.
272 |
273 | ## [[2.2.1] - 2022-05-23](https://github.com/MarwanAlsoltany/amqp-agent/compare/v2.2.0...v2.2.1)
274 |
275 | - Update `Consumer` class:
276 | * Fix an issue with `waitForAll()` method when the first channel is closed.
277 | * Refactor `waitForAll()` method.
278 | - Rebuild documentation.
279 |
--------------------------------------------------------------------------------
/src/Worker/AbstractWorker.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Connection\AMQPStreamConnection;
15 | use PhpAmqpLib\Channel\AMQPChannel;
16 | use PhpAmqpLib\Message\AMQPMessage;
17 | use PhpAmqpLib\Wire\AMQPTable;
18 | use PhpAmqpLib\Exception\AMQPInvalidArgumentException;
19 | use PhpAmqpLib\Exception\AMQPTimeoutException;
20 | use PhpAmqpLib\Exception\AMQPConnectionClosedException;
21 | use MAKS\AmqpAgent\Worker\AbstractWorkerInterface;
22 | use MAKS\AmqpAgent\Worker\WorkerCommandTrait;
23 | use MAKS\AmqpAgent\Worker\WorkerMutationTrait;
24 | use MAKS\AmqpAgent\Exception\MagicMethodsExceptionsTrait;
25 | use MAKS\AmqpAgent\Exception\PropertyDoesNotExistException;
26 | use MAKS\AmqpAgent\Exception\AmqpAgentException as Exception;
27 | use MAKS\AmqpAgent\Config\AbstractWorkerParameters as Parameters;
28 |
29 | /**
30 | * An abstract class implementing the basic functionality of a worker.
31 | * @since 1.0.0
32 | * @api
33 | */
34 | abstract class AbstractWorker implements AbstractWorkerInterface
35 | {
36 | use MagicMethodsExceptionsTrait {
37 | __get as private __get_MMET;
38 | __set as private __set_MMET;
39 | }
40 | use WorkerMutationTrait;
41 | use WorkerCommandTrait;
42 |
43 | /**
44 | * The default connection options that the worker should use when no overrides are provided.
45 | * @var array
46 | */
47 | protected $connectionOptions;
48 |
49 | /**
50 | * The default channel options that the worker should use when no overrides are provided.
51 | * @var array
52 | */
53 | protected $channelOptions;
54 |
55 | /**
56 | * The default queue options that the worker should use when no overrides are provided.
57 | * @var array
58 | */
59 | protected $queueOptions;
60 |
61 | /**
62 | * The default connection of the worker.
63 | * @var AMQPStreamConnection
64 | */
65 | public $connection;
66 |
67 | /**
68 | * The default channel of the worker.
69 | * @var AMQPChannel
70 | */
71 | public $channel;
72 |
73 | /**
74 | * All opened connections of the worker.
75 | * @var AMQPStreamConnection[]
76 | */
77 | public $connections = [];
78 |
79 | /**
80 | * All opened channels of the the worker.
81 | * @var AMQPChannel[]
82 | */
83 | public $channels = [];
84 |
85 |
86 | /**
87 | * AbstractWorker object constructor.
88 | * @param array $connectionOptions [optional] The overrides for the default connection options of the worker.
89 | * @param array $channelOptions [optional] The overrides for the default channel options of the worker.
90 | * @param array $queueOptions [optional] The overrides for the default queue options of the worker.
91 | */
92 | public function __construct(
93 | array $connectionOptions = [],
94 | array $channelOptions = [],
95 | array $queueOptions = []
96 | ) {
97 | $this->connectionOptions = Parameters::patch($connectionOptions, 'CONNECTION_OPTIONS');
98 | $this->channelOptions = Parameters::patch($channelOptions, 'CHANNEL_OPTIONS');
99 | $this->queueOptions = Parameters::patch($queueOptions, 'QUEUE_OPTIONS');
100 | }
101 |
102 | /**
103 | * Closes the connection with RabbitMQ server before destroying the object.
104 | */
105 | public function __destruct()
106 | {
107 | $this->disconnect();
108 | }
109 |
110 | /**
111 | * Gets a class member via public property access notation.
112 | * @param string $member Property name.
113 | * @return mixed
114 | * @throws PropertyDoesNotExistException
115 | */
116 | public function __get(string $member)
117 | {
118 | $isMember = property_exists($this, $member);
119 | if ($isMember) {
120 | return $this->{$member};
121 | }
122 |
123 | $this->__get_MMET($member);
124 | }
125 |
126 | /**
127 | * Sets a class member via public property assignment notation.
128 | * @param string $member Property name.
129 | * @param array $array Array of overrides. The array type here is important, because only *Options properties should be overridable.
130 | * @return void
131 | * @throws PropertyDoesNotExistException
132 | */
133 | public function __set(string $member, array $array)
134 | {
135 | $isMember = property_exists($this, $member);
136 | $notProtected = $member !== 'mutation';
137 |
138 | if ($isMember && $notProtected) {
139 | $acceptedKeys = array_keys($this->{$member});
140 | foreach ($array as $key => $value) {
141 | if (in_array($key, $acceptedKeys)) {
142 | $this->{$member}[$key] = $value;
143 | }
144 | }
145 | return;
146 | }
147 |
148 | $this->__set_MMET($member, $array);
149 | }
150 |
151 |
152 | /**
153 | * Closes the connection or the channel or both with RabbitMQ server.
154 | * @param AMQPStreamConnection|AMQPChannel|AMQPMessage ...$object The object that should be used to close the channel or the connection.
155 | * @return bool True on success.
156 | * @throws Exception
157 | */
158 | public static function shutdown(...$object): bool
159 | {
160 | $successful = true;
161 | $parameters = [];
162 |
163 | foreach ($object as $class) {
164 | $parameters[] = is_object($class) ? get_class($class) : gettype($class);
165 | if (
166 | $class instanceof AMQPStreamConnection ||
167 | $class instanceof AMQPChannel ||
168 | $class instanceof AMQPMessage
169 | ) {
170 | try {
171 | if (!($class instanceof AMQPMessage)) {
172 | $class->close();
173 | continue;
174 | }
175 | $class->getChannel()->close();
176 | } catch (AMQPConnectionClosedException $e) {
177 | // No need to throw the exception here as it's extraneous. This error
178 | // happens when a channel gets closed multiple times in different ways.
179 | }
180 | } else {
181 | $successful = false;
182 | }
183 | }
184 |
185 | if ($successful) {
186 | return $successful;
187 | }
188 |
189 | throw new Exception(
190 | sprintf(
191 | 'The passed parameter must be of type %s, %s or %s or a combination of them. Given parameter(s) has/have the type(s): %s!',
192 | AMQPStreamConnection::class,
193 | AMQPChannel::class,
194 | AMQPMessage::class,
195 | implode(', ', $parameters)
196 | )
197 | );
198 | }
199 |
200 | /**
201 | * Returns an AMQPTable object.
202 | * @param array $array An array of the option wished to be turn into the an arguments object.
203 | * @return AMQPTable
204 | */
205 | public static function arguments(array $array): AMQPTable
206 | {
207 | return new AMQPTable($array);
208 | }
209 |
210 |
211 | /**
212 | * Establishes a connection with RabbitMQ server and opens a channel for the worker in the opened connection, it also sets both of them as defaults.
213 | * @return self
214 | */
215 | public function connect()
216 | {
217 | if (empty($this->connection)) {
218 | $this->connection = $this->getNewConnection();
219 | }
220 |
221 | if (empty($this->channel)) {
222 | $this->channel = $this->getNewChannel();
223 | }
224 |
225 | return $this;
226 | }
227 |
228 | /**
229 | * Closes all open channels and connections with RabbitMQ server.
230 | * @return self
231 | */
232 | public function disconnect()
233 | {
234 | if (count($this->channels)) {
235 | foreach ($this->channels as $channel) {
236 | $channel->close();
237 | }
238 | $this->channel = null;
239 | $this->channels = [];
240 | }
241 |
242 | if (count($this->connections)) {
243 | foreach ($this->connections as $connection) {
244 | $connection->close();
245 | }
246 | $this->connection = null;
247 | $this->connections = [];
248 | }
249 |
250 | return $this;
251 | }
252 |
253 | /**
254 | * Executes `self::disconnect()` and `self::connect()` respectively. Note that this method will not restore old channels.
255 | * @return self
256 | */
257 | public function reconnect()
258 | {
259 | $this->disconnect();
260 | $this->connect();
261 |
262 | return $this;
263 | }
264 |
265 | /**
266 | * Declares a queue on the default channel of the worker's connection with RabbitMQ server.
267 | * @param array $parameters [optional] The overrides for the default queue options of the worker.
268 | * @param AMQPChannel $_channel [optional] The channel that should be used instead of the default worker's channel.
269 | * @return self
270 | * @throws AMQPTimeoutException
271 | */
272 | public function queue(?array $parameters = null, ?AMQPChannel $_channel = null)
273 | {
274 | $changes = null;
275 | if ($parameters) {
276 | $changes = $this->mutateClassMember('queueOptions', $parameters);
277 | }
278 |
279 | $channel = $_channel ?: $this->channel;
280 |
281 | try {
282 | $channel->queue_declare(
283 | $this->queueOptions['queue'],
284 | $this->queueOptions['passive'],
285 | $this->queueOptions['durable'],
286 | $this->queueOptions['exclusive'],
287 | $this->queueOptions['auto_delete'],
288 | $this->queueOptions['nowait'],
289 | $this->queueOptions['arguments'],
290 | $this->queueOptions['ticket']
291 | );
292 | } catch (AMQPTimeoutException $error) { // @codeCoverageIgnore
293 | Exception::rethrow($error); // @codeCoverageIgnore
294 | }
295 |
296 | if ($changes) {
297 | $this->mutateClassMember('queueOptions', $changes);
298 | }
299 |
300 | return $this;
301 | }
302 |
303 | /**
304 | * Returns the default connection of the worker. If the worker is not connected, it returns null.
305 | * @since 1.1.0
306 | * @return AMQPStreamConnection|null
307 | */
308 | public function getConnection(): ?AMQPStreamConnection
309 | {
310 | return $this->connection;
311 | }
312 |
313 | /**
314 | * Sets the passed connection as the default connection of the worker.
315 | * @since 1.1.0
316 | * @param AMQPStreamConnection $connection The connection that should be as the default connection of the worker.
317 | * @return self
318 | */
319 | public function setConnection(AMQPStreamConnection $connection)
320 | {
321 | $this->connection = $connection;
322 | return $this;
323 | }
324 |
325 | /**
326 | * Opens a new connection to RabbitMQ server and returns it. Connections returned by this method pushed to connections array and are not set as default automatically.
327 | * @since 1.1.0
328 | * @param array|null $parameters
329 | * @return AMQPStreamConnection
330 | */
331 | public function getNewConnection(array $parameters = null): AMQPStreamConnection
332 | {
333 | $changes = null;
334 | if ($parameters) {
335 | $changes = $this->mutateClassMember('connectionOptions', $parameters);
336 | }
337 |
338 | $this->connections[] = $connection = new AMQPStreamConnection(
339 | $this->connectionOptions['host'],
340 | $this->connectionOptions['port'],
341 | $this->connectionOptions['user'],
342 | $this->connectionOptions['password'],
343 | $this->connectionOptions['vhost'],
344 | $this->connectionOptions['insist'],
345 | $this->connectionOptions['login_method'],
346 | $this->connectionOptions['login_response'],
347 | $this->connectionOptions['locale'],
348 | $this->connectionOptions['connection_timeout'],
349 | $this->connectionOptions['read_write_timeout'],
350 | $this->connectionOptions['context'],
351 | $this->connectionOptions['keepalive'],
352 | $this->connectionOptions['heartbeat'],
353 | $this->connectionOptions['channel_rpc_timeout'],
354 | $this->connectionOptions['ssl_protocol']
355 | );
356 |
357 | if ($changes) {
358 | $this->mutateClassMember('connectionOptions', $changes);
359 | }
360 |
361 | return $connection;
362 | }
363 |
364 | /**
365 | * Returns the default channel of the worker. If the worker is not connected, it returns null.
366 | * @return AMQPChannel|null
367 | */
368 | public function getChannel(): ?AMQPChannel
369 | {
370 | return $this->channel;
371 | }
372 |
373 | /**
374 | * Sets the passed channel as the default channel of the worker.
375 | * @since 1.1.0
376 | * @param AMQPChannel $channel The channel that should be as the default channel of the worker.
377 | * @return self
378 | */
379 | public function setChannel(AMQPChannel $channel)
380 | {
381 | $this->channel = $channel;
382 | return $this;
383 | }
384 |
385 | /**
386 | * Returns a new channel on the the passed connection of the worker. If no connection is passed, it uses the default connection. If the worker is not connected, it returns null.
387 | * @param array|null $parameters [optional] The overrides for the default channel options of the worker.
388 | * @param AMQPStreamConnection|null $_connection [optional] The connection that should be used instead of the default worker's connection.
389 | * @return AMQPChannel|null
390 | */
391 | public function getNewChannel(array $parameters = null, ?AMQPStreamConnection $_connection = null): ?AMQPChannel
392 | {
393 | $changes = null;
394 | if ($parameters) {
395 | $changes = $this->mutateClassMember('channelOptions', $parameters);
396 | }
397 |
398 | $connection = $_connection ?: $this->connection;
399 |
400 | $channel = null;
401 | if (isset($connection)) {
402 | $this->channels[] = $channel = $connection->channel(
403 | $this->channelOptions['channel_id']
404 | );
405 | }
406 |
407 | if ($changes) {
408 | $this->mutateClassMember('channelOptions', $changes);
409 | }
410 |
411 | return $channel;
412 | }
413 |
414 | /**
415 | * Fetches a channel object identified by the passed id (channel_id). If not found, it returns null.
416 | * @param int $channelId The id of the channel wished to be fetched.
417 | * @param AMQPStreamConnection|null $_connection [optional] The connection that should be used instead of the default worker's connection.
418 | * @return AMQPChannel|null
419 | */
420 | public function getChannelById(int $channelId, ?AMQPStreamConnection $_connection = null): ?AMQPChannel
421 | {
422 | $connection = $_connection ?: $this->connection;
423 | $channels = $connection->channels;
424 |
425 | if (array_key_exists($channelId, $channels)) {
426 | return $channels[$channelId];
427 | }
428 |
429 | return null;
430 | }
431 | }
432 |
--------------------------------------------------------------------------------
/src/Worker/Publisher.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright Marwan Al-Soltany 2020
6 | * For the full copyright and license information, please view
7 | * the LICENSE file that was distributed with this source code.
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace MAKS\AmqpAgent\Worker;
13 |
14 | use PhpAmqpLib\Channel\AMQPChannel;
15 | use PhpAmqpLib\Message\AMQPMessage;
16 | use PhpAmqpLib\Exception\AMQPInvalidArgumentException;
17 | use PhpAmqpLib\Exception\AMQPTimeoutException;
18 | use PhpAmqpLib\Exception\AMQPConnectionBlockedException;
19 | use PhpAmqpLib\Exception\AMQPConnectionClosedException;
20 | use PhpAmqpLib\Exception\AMQPChannelClosedException;
21 | use MAKS\AmqpAgent\Worker\AbstractWorker;
22 | use MAKS\AmqpAgent\Worker\PublisherInterface;
23 | use MAKS\AmqpAgent\Worker\WorkerFacilitationInterface;
24 | use MAKS\AmqpAgent\Exception\AmqpAgentException as Exception;
25 | use MAKS\AmqpAgent\Config\PublisherParameters as Parameters;
26 |
27 | /**
28 | * A class specialized in publishing. Implementing only the methods needed for a publisher.
29 | *
30 | * Example:
31 | * ```
32 | * $publisher = new Publisher();
33 | * $publisher->connect();
34 | * $publisher->queue();
35 | * $publisher->exchange();
36 | * $publisher->bind();
37 | * $publisher->publish('Some message!');
38 | * $publisher->disconnect();
39 | * ```
40 | *
41 | * @since 1.0.0
42 | * @api
43 | */
44 | class Publisher extends AbstractWorker implements PublisherInterface, WorkerFacilitationInterface
45 | {
46 | /**
47 | * The default exchange options that the worker should use when no overrides are provided.
48 | * @var array
49 | */
50 | protected $exchangeOptions;
51 |
52 | /**
53 | * The default bind options that the worker should use when no overrides are provided.
54 | * @var array
55 | */
56 | protected $bindOptions;
57 |
58 | /**
59 | * The default message options that the worker should use when no overrides are provided.
60 | * @var array
61 | */
62 | protected $messageOptions;
63 |
64 | /**
65 | * The default publish options that the worker should use when no overrides are provided.
66 | * @var array
67 | */
68 | protected $publishOptions;
69 |
70 |
71 | /**
72 | * Publisher object constructor.
73 | * @param array $connectionOptions [optional] The overrides for the default connection options of the worker.
74 | * @param array $channelOptions [optional] The overrides for the default channel options of the worker.
75 | * @param array $queueOptions [optional] The overrides for the default queue options of the worker.
76 | * @param array $exchangeOptions [optional] The overrides for the default exchange options of the worker.
77 | * @param array $bindOptions [optional] The overrides for the default bind options of the worker.
78 | * @param array $messageOptions [optional] The overrides for the default message options of the worker.
79 | * @param array $publishOptions [optional] The overrides for the default publish options of the worker.
80 | */
81 | public function __construct(
82 | array $connectionOptions = [],
83 | array $channelOptions = [],
84 | array $queueOptions = [],
85 | array $exchangeOptions = [],
86 | array $bindOptions = [],
87 | array $messageOptions = [],
88 | array $publishOptions = []
89 | ) {
90 | $this->exchangeOptions = Parameters::patch($exchangeOptions, 'EXCHANGE_OPTIONS');
91 | $this->bindOptions = Parameters::patch($bindOptions, 'BIND_OPTIONS');
92 | $this->messageOptions = Parameters::patch($messageOptions, 'MESSAGE_OPTIONS');
93 | $this->publishOptions = Parameters::patch($publishOptions, 'PUBLISH_OPTIONS');
94 |
95 | parent::__construct($connectionOptions, $channelOptions, $queueOptions);
96 | }
97 |
98 |
99 | /**
100 | * Declares an exchange on the default channel of the worker's connection to RabbitMQ server.
101 | * @param array|null $parameters [optional] The overrides for the default exchange options of the worker.
102 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
103 | * @return self
104 | * @throws AMQPTimeoutException
105 | */
106 | public function exchange(?array $parameters = null, ?AMQPChannel $_channel = null)
107 | {
108 | $changes = null;
109 | if ($parameters) {
110 | $changes = $this->mutateClassMember('exchangeOptions', $parameters);
111 | }
112 |
113 | $channel = $_channel ?: $this->channel;
114 |
115 | try {
116 | $channel->exchange_declare(
117 | $this->exchangeOptions['exchange'],
118 | $this->exchangeOptions['type'],
119 | $this->exchangeOptions['passive'],
120 | $this->exchangeOptions['durable'],
121 | $this->exchangeOptions['auto_delete'],
122 | $this->exchangeOptions['internal'],
123 | $this->exchangeOptions['nowait'],
124 | $this->exchangeOptions['arguments'],
125 | $this->exchangeOptions['ticket']
126 | );
127 | } catch (AMQPTimeoutException $error) { // @codeCoverageIgnore
128 | Exception::rethrow($error); // @codeCoverageIgnore
129 | }
130 |
131 | if ($changes) {
132 | $this->mutateClassMember('exchangeOptions', $changes);
133 | }
134 |
135 | return $this;
136 | }
137 |
138 | /**
139 | * Binds the default queue to the default exchange on the default channel of the worker's connection to RabbitMQ server.
140 | * @param array|null $parameters [optional] The overrides for the default bind options of the worker.
141 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
142 | * @return self
143 | * @throws AMQPTimeoutException
144 | */
145 | public function bind(?array $parameters = null, ?AMQPChannel $_channel = null)
146 | {
147 | $changes = null;
148 | if ($parameters) {
149 | $changes = $this->mutateClassMember('bindOptions', $parameters);
150 | }
151 |
152 | $channel = $_channel ?: $this->channel;
153 |
154 | try {
155 | $channel->queue_bind(
156 | $this->bindOptions['queue'],
157 | $this->bindOptions['exchange'],
158 | $this->bindOptions['routing_key'],
159 | $this->bindOptions['nowait'],
160 | $this->bindOptions['arguments'],
161 | $this->bindOptions['ticket']
162 | );
163 | } catch (AMQPTimeoutException $error) { // @codeCoverageIgnore
164 | Exception::rethrow($error); // @codeCoverageIgnore
165 | }
166 |
167 | if ($changes) {
168 | $this->mutateClassMember('bindOptions', $changes);
169 | }
170 |
171 | return $this;
172 | }
173 |
174 | /**
175 | * Returns an AMQPMessage object.
176 | * @param string $body The body of the message.
177 | * @param array|null $properties [optional] The overrides for the default properties of the default message options of the worker.
178 | * @return AMQPMessage
179 | */
180 | public function message(string $body, ?array $properties = null): AMQPMessage
181 | {
182 | $changes = null;
183 | if ($properties) {
184 | $changes = $this->mutateClassSubMember('messageOptions', 'properties', $properties);
185 | }
186 |
187 | if ($body) {
188 | $this->messageOptions['body'] = $body;
189 | }
190 |
191 | $message = new AMQPMessage(
192 | $this->messageOptions['body'],
193 | $this->messageOptions['properties']
194 | );
195 |
196 | if ($changes) {
197 | $this->mutateClassSubMember('messageOptions', 'properties', $changes);
198 | }
199 |
200 | return $message;
201 | }
202 |
203 | /**
204 | * Publishes a message to the default exchange on the default channel of the worker's connection to RabbitMQ server.
205 | * @param string|array|AMQPMessage $payload A string of the body of the message or an array of body and properties for the message or a AMQPMessage object.
206 | * @param array|null $parameters [optional] The overrides for the default publish options of the worker.
207 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
208 | * @return self
209 | * @throws Exception|AMQPChannelClosedException|AMQPConnectionClosedException|AMQPConnectionBlockedException
210 | */
211 | public function publish($payload, ?array $parameters = null, ?AMQPChannel $_channel = null)
212 | {
213 | $changes = null;
214 | if ($parameters) {
215 | $changes = $this->mutateClassMember('publishOptions', $parameters);
216 | }
217 |
218 | $channel = $_channel ?: $this->channel;
219 |
220 | $originalMessage = $this->publishOptions['msg'];
221 |
222 | $message = $payload ?: $originalMessage;
223 |
224 | if ($message instanceof AMQPMessage) {
225 | $this->publishOptions['msg'] = $message;
226 | } elseif (is_array($message) && isset($message['body']) && isset($message['properties'])) {
227 | $this->publishOptions['msg'] = $this->message($message['body'], $message['properties']);
228 | } elseif (is_string($message)) {
229 | $this->publishOptions['msg'] = $this->message($message);
230 | } else {
231 | throw new Exception(
232 | sprintf(
233 | 'Payload must be a string, an array like %s, or an instance of "%s". The given parameter (data-type: %s) was none of them.',
234 | '["body" => "Message body!", "properties" ["key" => "value"]]',
235 | AMQPMessage::class,
236 | is_object($payload) ? get_class($payload) : gettype($payload)
237 | )
238 | );
239 | }
240 |
241 | try {
242 | $channel->basic_publish(
243 | $this->publishOptions['msg'],
244 | $this->publishOptions['exchange'],
245 | $this->publishOptions['routing_key'],
246 | $this->publishOptions['mandatory'],
247 | $this->publishOptions['immediate'],
248 | $this->publishOptions['ticket']
249 | );
250 | } catch (AMQPChannelClosedException | AMQPConnectionClosedException | AMQPConnectionBlockedException $error) { // @codeCoverageIgnore
251 | Exception::rethrow($error); // @codeCoverageIgnore
252 | } finally {
253 | // reverting messageOptions back to its state.
254 | $this->publishOptions['msg'] = $originalMessage;
255 | }
256 |
257 | if ($changes) {
258 | $this->mutateClassMember('publishOptions', $changes);
259 | }
260 |
261 | return $this;
262 | }
263 |
264 | /**
265 | * Publishes a batch of messages to the default exchange on the default channel of the worker's connection to RabbitMQ server.
266 | * @param string[]|array[]|AMQPMessage[] $messages An array of bodies of the messages or an array of arrays of body and properties for the messages or an array of AMQPMessage objects.
267 | * @param int $batchSize [optional] The number of messages that should be published per batch.
268 | * @param array|null $parameters [optional] The overrides for the default publish options of the worker.
269 | * @param AMQPChannel|null $_channel [optional] The channel that should be used instead of the default worker's channel.
270 | * @return self
271 | * @throws Exception|AMQPChannelClosedException|AMQPConnectionClosedException|AMQPConnectionBlockedException
272 | */
273 | public function publishBatch(array $messages, int $batchSize = 2500, ?array $parameters = null, ?AMQPChannel $_channel = null)
274 | {
275 | $changes = null;
276 | if ($parameters) {
277 | $changes = $this->mutateClassMember('publishOptions', $parameters);
278 | }
279 |
280 | $channel = $_channel ?: $this->channel;
281 |
282 | $originalMessage = $this->publishOptions['msg'];
283 |
284 | $count = count($messages);
285 | for ($i = 0; $i < $count; $i++) {
286 | $payload = $messages[$i];
287 |
288 | $message = $payload ?: $originalMessage;
289 |
290 | if ($message instanceof AMQPMessage) {
291 | $this->publishOptions['msg'] = $message;
292 | } elseif (is_array($message) && isset($message['body']) && isset($message['properties'])) {
293 | $this->publishOptions['msg'] = $this->message($message['body'], $message['properties']);
294 | } elseif (is_string($message)) {
295 | $this->publishOptions['msg'] = $this->message($message);
296 | } else {
297 | throw new Exception(
298 | sprintf(
299 | 'Messages array elements must be either a string, an array like %s, or an instance of "%s". Element in index "%d" (data-type: %s) was none of them.',
300 | '["body" => "Message body!", "properties" ["key" => "value"]]',
301 | AMQPMessage::class,
302 | $i,
303 | is_object($payload) ? get_class($payload) : gettype($payload)
304 | )
305 | );
306 | }
307 |
308 | $channel->batch_basic_publish(
309 | $this->publishOptions['msg'],
310 | $this->publishOptions['exchange'],
311 | $this->publishOptions['routing_key'],
312 | $this->publishOptions['mandatory'],
313 | $this->publishOptions['immediate'],
314 | $this->publishOptions['ticket']
315 | );
316 |
317 | if ($i % $batchSize == 0) {
318 | try {
319 | $channel->publish_batch();
320 | // @codeCoverageIgnoreStart
321 | } catch (AMQPConnectionBlockedException $e) {
322 | $tries = -1;
323 | do {
324 | sleep(1);
325 | $tries++;
326 | } while ($this->connection->isBlocked() && $tries >= 60);
327 |
328 | $channel->publish_batch();
329 | } catch (AMQPChannelClosedException | AMQPConnectionClosedException | AMQPConnectionBlockedException $error) {
330 | Exception::rethrow($error);
331 | // @codeCoverageIgnoreEnd
332 | }
333 | }
334 | }
335 |
336 | try {
337 | $channel->publish_batch();
338 | } catch (AMQPChannelClosedException | AMQPConnectionClosedException | AMQPConnectionBlockedException $error) { // @codeCoverageIgnore
339 | Exception::rethrow($error); // @codeCoverageIgnore
340 | } finally {
341 | // reverting messageOptions back to its state.
342 | $this->publishOptions['msg'] = $originalMessage;
343 | }
344 |
345 | if ($changes) {
346 | $this->mutateClassMember('publishOptions', $changes);
347 | }
348 |
349 | return $this;
350 | }
351 |
352 | /**
353 | * Executes `self::connect()`, `self::queue()`, `self::exchange`, and `self::bind()` respectively.
354 | * @return self
355 | */
356 | public function prepare()
357 | {
358 | $this->connect();
359 | $this->queue();
360 | $this->exchange();
361 | $this->bind();
362 |
363 | return $this;
364 | }
365 |
366 | /**
367 | * Executes `self::connect()`, `self::queue()`, `self::exchange`, `self::bind()`, `self::publish()`, and `self::disconnect()` respectively.
368 | * @param string[]|array[]|AMQPMessage[] $messages An array of strings, arrays, or AMQPMessage objects (same as `self::publishBatch()`).
369 | * @return void
370 | * @throws Exception
371 | */
372 | public function work($messages): void
373 | {
374 | try {
375 | $this->prepare();
376 | foreach ($messages as $message) {
377 | $this->publish($message);
378 | }
379 | $this->disconnect();
380 | } catch (Exception $error) {
381 | Exception::rethrow($error, null, false);
382 | }
383 | }
384 | }
385 |
--------------------------------------------------------------------------------