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