diff --git a/bin/subtree-split b/bin/subtree-split index c8ccdb259..2c3dd8261 100755 --- a/bin/subtree-split +++ b/bin/subtree-split @@ -11,7 +11,7 @@ function split() SHA1=`./bin/splitsh-lite --prefix=$1` - git push $2 "$SHA1:$CURRENT_BRANCH" + git push $2 "$SHA1:refs/heads/$CURRENT_BRANCH" } function split_new_repo() diff --git a/composer.json b/composer.json index acb627271..3345618b7 100644 --- a/composer.json +++ b/composer.json @@ -27,8 +27,8 @@ "enqueue/test": "*@dev", "enqueue/async-event-dispatcher": "*@dev", "queue-interop/queue-interop": "^0.6@dev", - "queue-interop/amqp-interop": "^0.6@dev", - "queue-interop/queue-spec": "^0.5@dev", + "queue-interop/amqp-interop": "^0.7@dev", + "queue-interop/queue-spec": "^0.5.1@dev", "phpunit/phpunit": "^5", "doctrine/doctrine-bundle": "~1.2", diff --git a/docker/Dockerfile b/docker/Dockerfile index fae39efd4..ffb51d818 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,6 +17,7 @@ RUN echo "extension=rdkafka.so" > /etc/php/7.1/cli/conf.d/10-rdkafka.ini RUN echo "extension=rdkafka.so" > /etc/php/7.1/fpm/conf.d/10-rdkafka.ini COPY ./php/cli.ini /etc/php/7.1/cli/conf.d/1-dev_cli.ini +COPY ./php/amqp.so /usr/lib/php/20160303/amqp.so COPY ./bin/dev_entrypoiny.sh /usr/local/bin/entrypoint.sh RUN chmod u+x /usr/local/bin/entrypoint.sh diff --git a/docker/php/amqp.so b/docker/php/amqp.so new file mode 100755 index 000000000..442c0bb49 Binary files /dev/null and b/docker/php/amqp.so differ diff --git a/docs/bundle/cli_commands.md b/docs/bundle/cli_commands.md index bf1166459..02225a105 100644 --- a/docs/bundle/cli_commands.md +++ b/docs/bundle/cli_commands.md @@ -29,6 +29,7 @@ Options: --setup-broker Creates queues, topics, exchanges, binding etc on broker side. --idle-timeout=IDLE-TIMEOUT The time in milliseconds queue consumer idle if no message has been received. --receive-timeout=RECEIVE-TIMEOUT The time in milliseconds queue consumer waits for a message. + --skip[=SKIP] Queues to skip consumption of messages from (multiple values allowed) -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version diff --git a/docs/bundle/config_reference.md b/docs/bundle/config_reference.md index 2dd7a888e..cac0ccd6b 100644 --- a/docs/bundle/config_reference.md +++ b/docs/bundle/config_reference.md @@ -39,148 +39,103 @@ enqueue: # The option tells whether RabbitMQ broker has delay plugin installed or not delay_plugin_installed: false amqp: + driver: ~ # One of "ext"; "lib"; "bunny" - # The connection to AMQP broker set as a string. Other parameters are ignored if set + # The connection to AMQP broker set as a string. Other parameters could be used as defaults dsn: ~ # The host to connect too. Note: Max 1024 characters - host: localhost + host: ~ # Port on the host. - port: 5672 + port: ~ # The user name to use. Note: Max 128 characters. - user: guest + user: ~ # Password. Note: Max 128 characters. - pass: guest + pass: ~ # The virtual host on the host. Note: Max 128 characters. - vhost: / + vhost: ~ # Connection timeout. Note: 0 or greater seconds. May be fractional. - connect_timeout: ~ + connection_timeout: ~ # Timeout in for income activity. Note: 0 or greater seconds. May be fractional. read_timeout: ~ # Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. write_timeout: ~ - persisted: false - lazy: true - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: basic_get # One of "basic_get"; "basic_consume" - rabbitmq_amqp: + # How often to send heartbeat. 0 means off. + heartbeat: ~ + persisted: ~ + lazy: ~ - # The connection to AMQP broker set as a string. Other parameters are ignored if set - dsn: ~ - - # The host to connect too. Note: Max 1024 characters - host: localhost - - # Port on the host. - port: 5672 - - # The user name to use. Note: Max 128 characters. - user: guest - - # Password. Note: Max 128 characters. - pass: guest - - # The virtual host on the host. Note: Max 128 characters. - vhost: / - - # Connection timeout. Note: 0 or greater seconds. May be fractional. - connect_timeout: ~ + # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher + receive_method: ~ # One of "basic_get"; "basic_consume" - # Timeout in for income activity. Note: 0 or greater seconds. May be fractional. - read_timeout: ~ + # The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit" + qos_prefetch_size: ~ - # Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. - write_timeout: ~ - persisted: false - lazy: true + # Specifies a prefetch window in terms of whole messages + qos_prefetch_count: ~ - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: basic_get # One of "basic_get"; "basic_consume" + # If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection. + qos_global: ~ - # The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id - delay_strategy: dlx - amqp_lib: + # The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option. + driver_options: ~ + rabbitmq_amqp: + driver: ~ # One of "ext"; "lib"; "bunny" - # The connection to AMQP broker set as a string. Other parameters are ignored if set + # The connection to AMQP broker set as a string. Other parameters could be used as defaults dsn: ~ # The host to connect too. Note: Max 1024 characters - host: localhost + host: ~ # Port on the host. - port: 5672 + port: ~ # The user name to use. Note: Max 128 characters. - user: guest + user: ~ # Password. Note: Max 128 characters. - pass: guest + pass: ~ # The virtual host on the host. Note: Max 128 characters. - vhost: / + vhost: ~ # Connection timeout. Note: 0 or greater seconds. May be fractional. - connection_timeout: !!float 3 - read_write_timeout: !!float 3 + connection_timeout: ~ # Timeout in for income activity. Note: 0 or greater seconds. May be fractional. - read_timeout: 3 + read_timeout: ~ # Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. - write_timeout: 3 - lazy: true - stream: true - insist: false - keepalive: false - - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: basic_get # One of "basic_get"; "basic_consume" - heartbeat: 0 - rabbitmq_amqp_lib: - - # The connection to AMQP broker set as a string. Other parameters are ignored if set - dsn: ~ - - # The host to connect too. Note: Max 1024 characters - host: localhost - - # Port on the host. - port: 5672 - - # The user name to use. Note: Max 128 characters. - user: guest + write_timeout: ~ - # Password. Note: Max 128 characters. - pass: guest + # How often to send heartbeat. 0 means off. + heartbeat: ~ + persisted: ~ + lazy: ~ - # The virtual host on the host. Note: Max 128 characters. - vhost: / + # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher + receive_method: ~ # One of "basic_get"; "basic_consume" - # Connection timeout. Note: 0 or greater seconds. May be fractional. - connection_timeout: !!float 3 - read_write_timeout: !!float 3 + # The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit" + qos_prefetch_size: ~ - # Timeout in for income activity. Note: 0 or greater seconds. May be fractional. - read_timeout: 3 + # Specifies a prefetch window in terms of whole messages + qos_prefetch_count: ~ - # Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. - write_timeout: 3 - lazy: true - stream: true - insist: false - keepalive: false + # If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection. + qos_global: ~ - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: basic_get # One of "basic_get"; "basic_consume" - heartbeat: 0 + # The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option. + driver_options: ~ # The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id delay_strategy: dlx @@ -197,6 +152,9 @@ enqueue: # The queue files are created with this given permissions if not exist. chmod: 384 + + # How often query for new messages. + polling_interval: 100 redis: # can be a host, or the path to a unix domain socket @@ -238,57 +196,6 @@ enqueue: # the connection will be performed as later as possible, if the option set to true lazy: true - amqp_bunny: - - # The connection to AMQP broker set as a string. Other parameters are ignored if set - dsn: ~ - - # The host to connect too. Note: Max 1024 characters - host: localhost - - # Port on the host. - port: 5672 - - # The user name to use. Note: Max 128 characters. - user: guest - - # Password. Note: Max 128 characters. - pass: guest - - # The virtual host on the host. Note: Max 128 characters. - vhost: / - lazy: true - - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: basic_get # One of "basic_get"; "basic_consume" - heartbeat: 0 - rabbitmq_amqp_bunny: - - # The connection to AMQP broker set as a string. Other parameters are ignored if set - dsn: ~ - - # The host to connect too. Note: Max 1024 characters - host: localhost - - # Port on the host. - port: 5672 - - # The user name to use. Note: Max 128 characters. - user: guest - - # Password. Note: Max 128 characters. - pass: guest - - # The virtual host on the host. Note: Max 128 characters. - vhost: / - lazy: true - - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: basic_get # One of "basic_get"; "basic_consume" - heartbeat: 0 - - # The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id - delay_strategy: dlx client: traceable_producer: false prefix: enqueue @@ -313,8 +220,6 @@ enqueue: doctrine_clear_identity_map_extension: false signal_extension: true reply_extension: true - - ``` [back to index](../index.md) diff --git a/docs/bundle/quick_tour.md b/docs/bundle/quick_tour.md index 9bd5e4b0d..cb9b0dcdf 100644 --- a/docs/bundle/quick_tour.md +++ b/docs/bundle/quick_tour.md @@ -6,7 +6,7 @@ It adds easy to use [configuration layer](config_reference.md), register service ## Install ```bash -$ composer require enqueue/enqueue-bundle enqueue/amqp-ext +$ composer require enqueue/enqueue-bundle enqueue/amqp-ext # or enqueue/amqp-bunny, enqueue/amqp-lib ``` _**Note**: You could use not only AMQP transport but other available: STOMP, Amazon SQS, Redis, Filesystem, Doctrine DBAL and others._ @@ -47,7 +47,7 @@ First, you have to configure a transport layer and set one to be default. enqueue: transport: - default: "amqp://" + default: "amqp:" client: ~ ``` diff --git a/docs/client/quick_tour.md b/docs/client/quick_tour.md index a5a4a3ed3..35e59537f 100644 --- a/docs/client/quick_tour.md +++ b/docs/client/quick_tour.md @@ -22,7 +22,7 @@ use Enqueue\SimpleClient\SimpleClient; include __DIR__.'/vendor/autoload.php'; -$client = new SimpleClient('amqp://'); +$client = new SimpleClient('amqp:'); ``` ## Produce message diff --git a/docs/client/rpc_call.md b/docs/client/rpc_call.md index 94a964d30..4afb1e722 100644 --- a/docs/client/rpc_call.md +++ b/docs/client/rpc_call.md @@ -1,55 +1,14 @@ # Client. RPC call The client's [quick tour](quick_tour.md) describes how to get the client object. -Here we'll use `Enqueue\SimpleClient\SimpleClient` though it is not required. -You can get all that stuff from manually built client or get objects from a container (Symfony). +Here we'll show you how to use Enqueue Client to perform a [RPC call](https://en.wikipedia.org/wiki/Remote_procedure_call). +You can do it by defining a command which returns something. -The simple client could be created like this: +## The consumer side -## The client side +On the consumer side we have to register a command processor which computes the result and send it back to the sender. +Pay attention that you have to add reply extension. It wont work without it. -There is a handy class RpcClient shipped with the client component. -It allows you to easily perform [RPC calls](https://en.wikipedia.org/wiki/Remote_procedure_call). -It send a message and wait for a reply. - -```php -getProducer(), $context); - -$replyMessage = $rpcClient->call('greeting_topic', 'Hi Thomas!', 5); -``` - -You can perform several requests asynchronously with `callAsync` and request replays later. - -```php -getProducer(), $context); - -$promises = []; -$promises[] = $rpcClient->callAsync('greeting_topic', 'Hi Thomas!', 5); -$promises[] = $rpcClient->callAsync('greeting_topic', 'Hi Thomas!', 5); -$promises[] = $rpcClient->callAsync('greeting_topic', 'Hi Thomas!', 5); -$promises[] = $rpcClient->callAsync('greeting_topic', 'Hi Thomas!', 5); - -$replyMessages = []; -foreach ($promises as $promise) { - $replyMessages[] = $promise->receive(); -} -``` - -## The server side - -On the server side you may register a processor which returns a result object with a reply message set. Of course it is possible to implement rpc server side based on transport classes only. That would require a bit more work to do. ```php @@ -60,19 +19,61 @@ use Interop\Queue\PsrContext; use Enqueue\Consumption\Result; use Enqueue\Consumption\ChainExtension; use Enqueue\Consumption\Extension\ReplyExtension; +use Enqueue\Client\Config; use Enqueue\SimpleClient\SimpleClient; /** @var \Interop\Queue\PsrContext $context */ -$client = new SimpleClient('amqp://'); +// composer require enqueue/amqp-ext # or enqueue/amqp-bunny, enqueue/amqp-lib +$client = new SimpleClient('amqp:'); -$client->bind('greeting_topic', 'greeting_processor', function (PsrMessage $message, PsrContext $context) use (&$requestMessage) { - echo $message->getBody(); +$client->bind(Config::COMMAND_TOPIC, 'square', function (PsrMessage $message, PsrContext $context) use (&$requestMessage) { + $number = (int) $message->getBody(); - return Result::reply($context->createMessage('Hi there! I am John.')); + return Result::reply($context->createMessage($number ^ 2)); }); $client->consume(new ChainExtension([new ReplyExtension()])); ``` [back to index](../index.md) + +## The sender side + +On the sender's side we need a client which send a command and wait for reply messages. + +```php +sendCommand('square', 5, true)->receive(5000 /* 5 sec */)->getBody(); +``` + +You can perform several requests asynchronously with `sendCommand` and ask for replays later. + +```php +sendCommand('square', 5, true); +$promises[] = $client->sendCommand('square', 10, true); +$promises[] = $client->sendCommand('square', 7, true); +$promises[] = $client->sendCommand('square', 12, true); + +$replyMessages = []; +while ($promises) { + foreach ($promises as $index => $promise) { + if ($replyMessage = $promise->receiveNoWait()) { + $replyMessages[$index] = $replyMessage; + + unset($promises[$index]); + } + } +} +``` \ No newline at end of file diff --git a/docs/elastica-bundle/populate-command-optimization.md b/docs/elastica-bundle/populate-command-optimization.md index ccaa355b5..58839bd08 100644 --- a/docs/elastica-bundle/populate-command-optimization.md +++ b/docs/elastica-bundle/populate-command-optimization.md @@ -16,6 +16,8 @@ Add bundles to `AppKernel` ```php \Enqueue\AmqpBunny\AmqpConnectionFactory::class, // connects to localhost - 'dsn' => 'amqp://', + 'dsn' => 'amqp:', // could be "rabbitmq_dlx", "rabbitmq_delay_plugin", instance of DelayStrategy interface or null // 'delay_strategy' => 'rabbitmq_dlx' diff --git a/docs/quick_tour.md b/docs/quick_tour.md index 0be831500..9a5eb4fa9 100644 --- a/docs/quick_tour.md +++ b/docs/quick_tour.md @@ -171,7 +171,7 @@ use Enqueue\SimpleClient\SimpleClient; use Interop\Queue\PsrMessage; // composer require enqueue/amqp-ext -$client = new SimpleClient('amqp://'); +$client = new SimpleClient('amqp:'); // composer require enqueue/fs $client = new SimpleClient('file://foo/bar'); @@ -197,8 +197,8 @@ use Enqueue\Client\Config; use Enqueue\Consumption\Extension\ReplyExtension; use Enqueue\Consumption\Result; -// composer require enqueue/amqp-ext -$client = new SimpleClient('amqp://'); +// composer require enqueue/amqp-ext # or enqueue/amqp-bunny or enqueue/amqp-lib +$client = new SimpleClient('amqp:'); // composer require enqueue/fs $client = new SimpleClient('file://foo/bar'); diff --git a/docs/transport/amqp.md b/docs/transport/amqp.md index 8fd374521..fcfdd1436 100644 --- a/docs/transport/amqp.md +++ b/docs/transport/amqp.md @@ -32,13 +32,13 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; $connectionFactory = new AmqpConnectionFactory(); // same as above -$connectionFactory = new AmqpConnectionFactory('amqp://'); +$factory = new AmqpConnectionFactory('amqp:'); // same as above -$connectionFactory = new AmqpConnectionFactory([]); +$factory = new AmqpConnectionFactory([]); // connect to AMQP broker at example.com -$connectionFactory = new AmqpConnectionFactory([ +$factory = new AmqpConnectionFactory([ 'host' => 'example.com', 'port' => 1000, 'vhost' => '/', @@ -48,9 +48,13 @@ $connectionFactory = new AmqpConnectionFactory([ ]); // same as above but given as DSN string -$connectionFactory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); +$factory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); -$psrContext = $connectionFactory->createContext(); +$psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('amqp:'); +$psrContext = \Enqueue\dsn_to_context('amqp+ext:'); ``` ## Declare topic. diff --git a/docs/transport/amqp_bunny.md b/docs/transport/amqp_bunny.md index 28c78c8cc..09b59336e 100644 --- a/docs/transport/amqp_bunny.md +++ b/docs/transport/amqp_bunny.md @@ -29,16 +29,16 @@ $ composer require enqueue/amqp-bunny use Enqueue\AmqpBunny\AmqpConnectionFactory; // connects to localhost -$connectionFactory = new AmqpConnectionFactory(); +$factory = new AmqpConnectionFactory(); // same as above -$connectionFactory = new AmqpConnectionFactory('amqp://'); +$factory = new AmqpConnectionFactory('amqp:'); // same as above -$connectionFactory = new AmqpConnectionFactory([]); +$factory = new AmqpConnectionFactory([]); // connect to AMQP broker at example.com -$connectionFactory = new AmqpConnectionFactory([ +$factory = new AmqpConnectionFactory([ 'host' => 'example.com', 'port' => 1000, 'vhost' => '/', @@ -48,9 +48,13 @@ $connectionFactory = new AmqpConnectionFactory([ ]); // same as above but given as DSN string -$connectionFactory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); +$factory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); -$psrContext = $connectionFactory->createContext(); +$psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('amqp:'); +$psrContext = \Enqueue\dsn_to_context('amqp+bunny:'); ``` ## Declare topic. diff --git a/docs/transport/amqp_lib.md b/docs/transport/amqp_lib.md index 59061352d..8ce661433 100644 --- a/docs/transport/amqp_lib.md +++ b/docs/transport/amqp_lib.md @@ -29,16 +29,16 @@ $ composer require enqueue/amqp-lib use Enqueue\AmqpLib\AmqpConnectionFactory; // connects to localhost -$connectionFactory = new AmqpConnectionFactory(); +$factory = new AmqpConnectionFactory(); // same as above -$connectionFactory = new AmqpConnectionFactory('amqp://'); +$factory = new AmqpConnectionFactory('amqp:'); // same as above -$connectionFactory = new AmqpConnectionFactory([]); +$factory = new AmqpConnectionFactory([]); // connect to AMQP broker at example.com -$connectionFactory = new AmqpConnectionFactory([ +$factory = new AmqpConnectionFactory([ 'host' => 'example.com', 'port' => 1000, 'vhost' => '/', @@ -48,9 +48,13 @@ $connectionFactory = new AmqpConnectionFactory([ ]); // same as above but given as DSN string -$connectionFactory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); +$factory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); -$psrContext = $connectionFactory->createContext(); +$psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('amqp:'); +$psrContext = \Enqueue\dsn_to_context('amqp+lib:'); ``` ## Declare topic. diff --git a/docs/transport/dbal.md b/docs/transport/dbal.md index 9f39bc729..2d84782e1 100644 --- a/docs/transport/dbal.md +++ b/docs/transport/dbal.md @@ -28,6 +28,9 @@ use Enqueue\Dbal\DbalConnectionFactory; $factory = new DbalConnectionFactory('mysql://user:pass@localhost:3306/mqdev'); +// connects to localhost +$factory = new DbalConnectionFactory('mysql:'); + $psrContext = $factory->createContext(); ``` @@ -45,6 +48,9 @@ $factory = new ManagerRegistryConnectionFactory($registry, [ ]); $psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('mysql:'); ``` ## Init database diff --git a/docs/transport/filesystem.md b/docs/transport/filesystem.md index 3b529eadc..825b7b5a2 100644 --- a/docs/transport/filesystem.md +++ b/docs/transport/filesystem.md @@ -29,7 +29,7 @@ use Enqueue\Fs\FsConnectionFactory; $connectionFactory = new FsConnectionFactory(); // same as above -$connectionFactory = new FsConnectionFactory('file://'); +$connectionFactory = new FsConnectionFactory('file:'); // stores in custom folder $connectionFactory = new FsConnectionFactory('/path/to/queue/dir'); @@ -47,6 +47,9 @@ $connectionFactory = new FsConnectionFactory([ ]); $psrContext = $connectionFactory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('file:'); ``` ## Send message to topic diff --git a/docs/transport/gearman.md b/docs/transport/gearman.md index a9dc0dc24..0161048d5 100644 --- a/docs/transport/gearman.md +++ b/docs/transport/gearman.md @@ -26,7 +26,7 @@ use Enqueue\Gearman\GearmanConnectionFactory; $factory = new GearmanConnectionFactory(); // same as above -$factory = new GearmanConnectionFactory('gearman://'); +$factory = new GearmanConnectionFactory('gearman:'); // connects to example host and port 5555 $factory = new GearmanConnectionFactory('gearman://example:5555'); @@ -36,6 +36,11 @@ $factory = new GearmanConnectionFactory([ 'host' => 'example', 'port' => 5555 ]); + +$psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('gearman:'); ``` ## Send message to topic diff --git a/docs/transport/gps.md b/docs/transport/gps.md index f124feb9b..7d0197f23 100644 --- a/docs/transport/gps.md +++ b/docs/transport/gps.md @@ -27,7 +27,13 @@ putenv('PUBSUB_EMULATOR_HOST=http://localhost:8900'); $connectionFactory = new GpsConnectionFactory(); +// save as above +$connectionFactory = new GpsConnectionFactory('gps:'); + $psrContext = $connectionFactory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('gps:'); ``` ## Send message to topic diff --git a/docs/transport/kafka.md b/docs/transport/kafka.md index 915f1b08b..efc564fa2 100644 --- a/docs/transport/kafka.md +++ b/docs/transport/kafka.md @@ -25,7 +25,7 @@ use Enqueue\RdKafka\RdKafkaConnectionFactory; $connectionFactory = new RdKafkaConnectionFactory(); // same as above -$connectionFactory = new RdKafkaConnectionFactory('rdkafka://'); +$connectionFactory = new RdKafkaConnectionFactory('kafka:'); // same as above $connectionFactory = new RdKafkaConnectionFactory([]); @@ -43,6 +43,9 @@ $connectionFactory = new RdKafkaConnectionFactory([ ]); $psrContext = $connectionFactory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('kafka:'); ``` ## Send message to topic diff --git a/docs/transport/pheanstalk.md b/docs/transport/pheanstalk.md index 559517c00..4371c2966 100644 --- a/docs/transport/pheanstalk.md +++ b/docs/transport/pheanstalk.md @@ -26,7 +26,7 @@ use Enqueue\Pheanstalk\PheanstalkConnectionFactory; $factory = new PheanstalkConnectionFactory(); // same as above -$factory = new PheanstalkConnectionFactory('beanstalk://'); +$factory = new PheanstalkConnectionFactory('beanstalk:'); // connects to example host and port 5555 $factory = new PheanstalkConnectionFactory('beanstalk://example:5555'); @@ -36,6 +36,11 @@ $factory = new PheanstalkConnectionFactory([ 'host' => 'example', 'port' => 5555 ]); + +$psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('beanstalk:'); ``` ## Send message to topic diff --git a/docs/transport/redis.md b/docs/transport/redis.md index 3ded64a36..0b362217a 100644 --- a/docs/transport/redis.md +++ b/docs/transport/redis.md @@ -58,6 +58,9 @@ $factory = new RedisConnectionFactory([ $factory = new RedisConnectionFactory('redis://example.com:1000?vendor=phpredis'); $psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('redis:'); ``` * With predis library: diff --git a/docs/transport/sqs.md b/docs/transport/sqs.md index 2c35c8bc3..03da0ebeb 100644 --- a/docs/transport/sqs.md +++ b/docs/transport/sqs.md @@ -33,6 +33,9 @@ $factory = new SqsConnectionFactory([ $factory = new SqsConnectionFactory('sqs:?key=aKey&secret=aSecret®ion=aRegion'); $psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('sqs:'); ``` ## Declare queue. diff --git a/docs/transport/stomp.md b/docs/transport/stomp.md index 1b40f96d1..fb9ea7694 100644 --- a/docs/transport/stomp.md +++ b/docs/transport/stomp.md @@ -38,6 +38,9 @@ $factory = new StompConnectionFactory([ $factory = new StompConnectionFactory('stomp://example.com:1000?login=theLogin'); $psrContext = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a function from there to create the context +$psrContext = \Enqueue\dsn_to_context('stomp:'); ``` ## Send message to topic diff --git a/phpstan.neon b/phpstan.neon index 8dd052e94..ecb473cfd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,4 +7,5 @@ parameters: - pkg/redis/PhpRedis.php - pkg/redis/RedisConnectionFactory.php - pkg/gearman - - pkg/amqp-ext/AmqpConsumer.php \ No newline at end of file + - pkg/amqp-ext/AmqpConsumer.php + - pkg/amqp-ext/AmqpContext.php \ No newline at end of file diff --git a/pkg/amqp-bunny/AmqpConnectionFactory.php b/pkg/amqp-bunny/AmqpConnectionFactory.php index 48f5fe742..470819778 100644 --- a/pkg/amqp-bunny/AmqpConnectionFactory.php +++ b/pkg/amqp-bunny/AmqpConnectionFactory.php @@ -2,7 +2,7 @@ namespace Enqueue\AmqpBunny; -use Bunny\Client; +use Enqueue\AmqpTools\ConnectionConfig; use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; use Interop\Amqp\AmqpConnectionFactory as InteropAmqpConnectionFactory; @@ -12,60 +12,37 @@ class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrate use DelayStrategyAwareTrait; /** - * @var array + * @var ConnectionConfig */ private $config; /** - * @var Client + * @var BunnyClient */ private $client; /** - * The config could be an array, string DSN or null. In case of null it will attempt to connect to localhost with default credentials. + * @see \Enqueue\AmqpTools\ConnectionConfig for possible config formats and values * - * [ - * 'host' => 'amqp.host The host to connect too. Note: Max 1024 characters.', - * 'port' => 'amqp.port Port on the host.', - * 'vhost' => 'amqp.vhost The virtual host on the host. Note: Max 128 characters.', - * 'user' => 'amqp.user The user name to use. Note: Max 128 characters.', - * 'pass' => 'amqp.password Password. Note: Max 128 characters.', - * 'lazy' => 'the connection will be performed as later as possible, if the option set to true', - * 'receive_method' => 'Could be either basic_get or basic_consume', - * 'qos_prefetch_size' => 'The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit"', - * 'qos_prefetch_count' => 'Specifies a prefetch window in terms of whole messages.', - * 'qos_global' => 'If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection.', - * ] + * In addition this factory accepts next options: + * receive_method - Could be either basic_get or basic_consume * - * or - * - * amqp://user:pass@host:10000/vhost?lazy=true&socket=true - * - * @param array|string $config + * @param array|string|null $config */ - public function __construct($config = 'amqp://') + public function __construct($config = 'amqp:') { - if (is_string($config) && 0 === strpos($config, 'amqp+bunny:')) { - $config = str_replace('amqp+bunny:', 'amqp:', $config); - } - - // third argument is deprecated will be removed in 0.8 - if (empty($config) || 'amqp:' === $config || 'amqp://' === $config) { - $config = []; - } elseif (is_string($config)) { - $config = $this->parseDsn($config); - } elseif (is_array($config)) { - } else { - throw new \LogicException('The config must be either an array of options, a DSN string or null'); - } - - $this->config = array_replace($this->defaultConfig(), $config); + $this->config = (new ConnectionConfig($config)) + ->addSupportedScheme('amqp+bunny') + ->addDefaultOption('receive_method', 'basic_get') + ->addDefaultOption('tcp_nodelay', null) + ->parse() + ; $supportedMethods = ['basic_get', 'basic_consume']; - if (false == in_array($this->config['receive_method'], $supportedMethods, true)) { + if (false == in_array($this->config->getOption('receive_method'), $supportedMethods, true)) { throw new \LogicException(sprintf( 'Invalid "receive_method" option value "%s". It could be only "%s"', - $this->config['receive_method'], + $this->config->getOption('receive_method'), implode('", "', $supportedMethods) )); } @@ -76,95 +53,66 @@ public function __construct($config = 'amqp://') */ public function createContext() { - if ($this->config['lazy']) { + if ($this->config->isLazy()) { $context = new AmqpContext(function () { - return $this->establishConnection()->channel(); - }, $this->config); + $channel = $this->establishConnection()->channel(); + $channel->qos($this->config->getQosPrefetchSize(), $this->config->getQosPrefetchCount(), $this->config->isQosGlobal()); + + return $channel; + }, $this->config->getConfig()); $context->setDelayStrategy($this->delayStrategy); return $context; } - $context = new AmqpContext($this->establishConnection()->channel(), $this->config); + $context = new AmqpContext($this->establishConnection()->channel(), $this->config->getConfig()); $context->setDelayStrategy($this->delayStrategy); + $context->setQos($this->config->getQosPrefetchSize(), $this->config->getQosPrefetchCount(), $this->config->isQosGlobal()); return $context; } /** - * @return Client + * @return ConnectionConfig */ - private function establishConnection() + public function getConfig() { - if (false == $this->client) { - $this->client = new Client($this->config); - $this->client->connect(); - } - - return $this->client; + return $this->config; } /** - * @param string $dsn - * - * @return array + * @return BunnyClient */ - private function parseDsn($dsn) + private function establishConnection() { - $dsnConfig = parse_url($dsn); - if (false === $dsnConfig) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); - } - - $dsnConfig = array_replace([ - 'scheme' => null, - 'host' => null, - 'port' => null, - 'user' => null, - 'pass' => null, - 'path' => null, - 'query' => null, - ], $dsnConfig); - - if ('amqp' !== $dsnConfig['scheme']) { - throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be "amqp" only.', $dsnConfig['scheme'])); - } - - if ($dsnConfig['query']) { - $query = []; - parse_str($dsnConfig['query'], $query); - - $dsnConfig = array_replace($query, $dsnConfig); + if (false == $this->client) { + $bunnyConfig = []; + $bunnyConfig['host'] = $this->config->getHost(); + $bunnyConfig['port'] = $this->config->getPort(); + $bunnyConfig['vhost'] = $this->config->getVHost(); + $bunnyConfig['user'] = $this->config->getUser(); + $bunnyConfig['password'] = $this->config->getPass(); + $bunnyConfig['read_write_timeout'] = min($this->config->getReadTimeout(), $this->config->getWriteTimeout()); + $bunnyConfig['timeout'] = $this->config->getConnectionTimeout(); + + // @see https://github.com/php-enqueue/enqueue-dev/issues/229 +// $bunnyConfig['persistent'] = $this->config->isPersisted(); +// if ($this->config->isPersisted()) { +// $bunnyConfig['path'] = 'enqueue';//$this->config->getOption('path', $this->config->getOption('vhost')); +// } + + if ($this->config->getHeartbeat()) { + $bunnyConfig['heartbeat'] = $this->config->getHeartbeat(); + } + + if (null !== $this->config->getOption('tcp_nodelay')) { + $bunnyConfig['tcp_nodelay'] = $this->config->getOption('tcp_nodelay'); + } + + $this->client = new BunnyClient($bunnyConfig); + $this->client->connect(); } - $dsnConfig['vhost'] = ltrim($dsnConfig['path'], '/'); - - unset($dsnConfig['scheme'], $dsnConfig['query'], $dsnConfig['fragment'], $dsnConfig['path']); - - $dsnConfig = array_map(function ($value) { - return urldecode($value); - }, $dsnConfig); - - return $dsnConfig; - } - - /** - * @return array - */ - private function defaultConfig() - { - return [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'lazy' => true, - 'vhost' => '/', - 'heartbeat' => 0, - 'receive_method' => 'basic_get', - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ]; + return $this->client; } } diff --git a/pkg/amqp-bunny/AmqpConsumer.php b/pkg/amqp-bunny/AmqpConsumer.php index 07b361d2e..0bfd0b94a 100644 --- a/pkg/amqp-bunny/AmqpConsumer.php +++ b/pkg/amqp-bunny/AmqpConsumer.php @@ -3,18 +3,20 @@ namespace Enqueue\AmqpBunny; use Bunny\Channel; -use Bunny\Client; use Bunny\Message; use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; -use Interop\Amqp\Impl\AmqpMessage; -use Interop\Queue\Exception; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrMessage; class AmqpConsumer implements InteropAmqpConsumer { + /** + * @var AmqpContext + */ + private $context; + /** * @var Channel */ @@ -30,11 +32,6 @@ class AmqpConsumer implements InteropAmqpConsumer */ private $buffer; - /** - * @var bool - */ - private $isInit; - /** * @var string */ @@ -51,25 +48,19 @@ class AmqpConsumer implements InteropAmqpConsumer private $consumerTag; /** - * @var Message - */ - private $bunnyMessages = []; - - /** - * @param Channel $channel + * @param AmqpContext $context * @param InteropAmqpQueue $queue * @param Buffer $buffer * @param string $receiveMethod */ - public function __construct(Channel $channel, InteropAmqpQueue $queue, Buffer $buffer, $receiveMethod) + public function __construct(AmqpContext $context, InteropAmqpQueue $queue, Buffer $buffer, $receiveMethod) { - $this->channel = $channel; + $this->context = $context; + $this->channel = $context->getBunnyChannel(); $this->queue = $queue; $this->buffer = $buffer; $this->receiveMethod = $receiveMethod; $this->flags = self::FLAG_NOPARAM; - - $this->isInit = false; } /** @@ -77,10 +68,6 @@ public function __construct(Channel $channel, InteropAmqpQueue $queue, Buffer $b */ public function setConsumerTag($consumerTag) { - if ($this->isInit) { - throw new Exception('Consumer tag is not mutable after it has been subscribed to broker'); - } - $this->consumerTag = $consumerTag; } @@ -154,9 +141,7 @@ public function receive($timeout = 0) public function receiveNoWait() { if ($message = $this->channel->get($this->queue->getQueueName(), (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOACK))) { - $this->bunnyMessages[$message->deliveryTag] = $message; - - return $this->convertMessage($message); + return $this->context->convertMessage($message); } } @@ -167,11 +152,8 @@ public function acknowledge(PsrMessage $message) { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); - if (isset($this->bunnyMessages[$message->getDeliveryTag()])) { - $this->channel->ack($this->bunnyMessages[$message->getDeliveryTag()]); - - unset($this->bunnyMessages[$message->getDeliveryTag()]); - } + $bunnyMessage = new Message('', $message->getDeliveryTag(), '', '', '', [], ''); + $this->channel->ack($bunnyMessage); } /** @@ -182,41 +164,8 @@ public function reject(PsrMessage $message, $requeue = false) { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); - if (isset($this->bunnyMessages[$message->getDeliveryTag()])) { - $this->channel->reject($this->bunnyMessages[$message->getDeliveryTag()], $requeue); - - unset($this->bunnyMessages[$message->getDeliveryTag()]); - } - } - - /** - * @param Message $bunnyMessage - * - * @return InteropAmqpMessage - */ - private function convertMessage(Message $bunnyMessage) - { - $headers = $bunnyMessage->headers; - - $properties = []; - if (isset($headers['application_headers'])) { - $properties = $headers['application_headers']; - } - unset($headers['application_headers']); - - if (array_key_exists('timestamp', $headers)) { - /** @var \DateTime $date */ - $date = $headers['timestamp']; - - $headers['timestamp'] = (int) $date->format('U'); - } - - $message = new AmqpMessage($bunnyMessage->content, $properties, $headers); - $message->setDeliveryTag($bunnyMessage->deliveryTag); - $message->setRedelivered($bunnyMessage->redelivered); - $message->setRoutingKey($bunnyMessage->routingKey); - - return $message; + $bunnyMessage = new Message('', $message->getDeliveryTag(), '', '', '', [], ''); + $this->channel->reject($bunnyMessage, $requeue); } /** @@ -244,34 +193,12 @@ private function receiveBasicGet($timeout) */ private function receiveBasicConsume($timeout) { - if (false === $this->isInit) { - $callback = function (Message $message, Channel $channel, Client $bunny) { - $receivedMessage = $this->convertMessage($message); - $receivedMessage->setConsumerTag($message->consumerTag); - - $this->bunnyMessages[$message->deliveryTag] = $message; - $this->buffer->push($receivedMessage->getConsumerTag(), $receivedMessage); - - $bunny->stop(); - }; - - $frame = $this->channel->consume( - $callback, - $this->queue->getQueueName(), - $this->getConsumerTag() ?: $this->getQueue()->getConsumerTag(), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOACK), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT) - ); - - $this->consumerTag = $frame->consumerTag; - - if (empty($this->consumerTag)) { - throw new Exception('Got empty consumer tag'); - } + if (false == $this->consumerTag) { + $this->context->subscribe($this, function (InteropAmqpMessage $message) { + $this->buffer->push($message->getConsumerTag(), $message); - $this->isInit = true; + return false; + }); } if ($message = $this->buffer->pop($this->consumerTag)) { @@ -281,7 +208,7 @@ private function receiveBasicConsume($timeout) while (true) { $start = microtime(true); - $this->channel->getClient()->run($timeout / 1000); + $this->context->consume($timeout); if ($message = $this->buffer->pop($this->consumerTag)) { return $message; diff --git a/pkg/amqp-bunny/AmqpContext.php b/pkg/amqp-bunny/AmqpContext.php index 84508e856..e034d75fe 100644 --- a/pkg/amqp-bunny/AmqpContext.php +++ b/pkg/amqp-bunny/AmqpContext.php @@ -3,9 +3,12 @@ namespace Enqueue\AmqpBunny; use Bunny\Channel; +use Bunny\Client; +use Bunny\Message; use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; use Interop\Amqp\AmqpBind as InteropAmqpBind; +use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpContext as InteropAmqpContext; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; @@ -43,6 +46,13 @@ class AmqpContext implements InteropAmqpContext, DelayStrategyAware */ private $buffer; + /** + * an item contains an array: [AmqpConsumerInterop $consumer, callable $callback];. + * + * @var array + */ + private $subscribers; + /** * Callable must return instance of \Bunny\Channel once called. * @@ -117,10 +127,10 @@ public function createConsumer(PsrDestination $destination) $queue = $this->createTemporaryQueue(); $this->bind(new AmqpBind($destination, $queue, $queue->getQueueName())); - return new AmqpConsumer($this->getBunnyChannel(), $queue, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $queue, $this->buffer, $this->config['receive_method']); } - return new AmqpConsumer($this->getBunnyChannel(), $destination, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $destination, $this->buffer, $this->config['receive_method']); } /** @@ -309,6 +319,77 @@ public function setQos($prefetchSize, $prefetchCount, $global) $this->getBunnyChannel()->qos($prefetchSize, $prefetchCount, $global); } + /** + * {@inheritdoc} + */ + public function subscribe(InteropAmqpConsumer $consumer, callable $callback) + { + if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { + return; + } + + $bunnyCallback = function (Message $message, Channel $channel, Client $bunny) { + $receivedMessage = $this->convertMessage($message); + $receivedMessage->setConsumerTag($message->consumerTag); + + /** + * @var AmqpConsumer + * @var callable $callback + */ + list($consumer, $callback) = $this->subscribers[$message->consumerTag]; + + if (false === call_user_func($callback, $receivedMessage, $consumer)) { + $bunny->stop(); + } + }; + + $frame = $this->getBunnyChannel()->consume( + $bunnyCallback, + $consumer->getQueue()->getQueueName(), + $consumer->getConsumerTag(), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOACK), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT) + ); + + if (empty($frame->consumerTag)) { + throw new Exception('Got empty consumer tag'); + } + + $consumer->setConsumerTag($frame->consumerTag); + + $this->subscribers[$frame->consumerTag] = [$consumer, $callback]; + } + + /** + * {@inheritdoc} + */ + public function unsubscribe(InteropAmqpConsumer $consumer) + { + if (false == $consumer->getConsumerTag()) { + return; + } + + $consumerTag = $consumer->getConsumerTag(); + + $this->getBunnyChannel()->cancel($consumerTag); + $consumer->setConsumerTag(null); + unset($this->subscribers[$consumerTag]); + } + + /** + * {@inheritdoc} + */ + public function consume($timeout = 0) + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + $this->getBunnyChannel()->getClient()->run($timeout / 1000); + } + /** * @return Channel */ @@ -328,4 +409,36 @@ public function getBunnyChannel() return $this->bunnyChannel; } + + /** + * @internal It must be used here and in the consumer only + * + * @param Message $bunnyMessage + * + * @return InteropAmqpMessage + */ + public function convertMessage(Message $bunnyMessage) + { + $headers = $bunnyMessage->headers; + + $properties = []; + if (isset($headers['application_headers'])) { + $properties = $headers['application_headers']; + } + unset($headers['application_headers']); + + if (array_key_exists('timestamp', $headers) && $headers['timestamp']) { + /** @var \DateTime $date */ + $date = $headers['timestamp']; + + $headers['timestamp'] = (int) $date->format('U'); + } + + $message = new AmqpMessage($bunnyMessage->content, $properties, $headers); + $message->setDeliveryTag($bunnyMessage->deliveryTag); + $message->setRedelivered($bunnyMessage->redelivered); + $message->setRoutingKey($bunnyMessage->routingKey); + + return $message; + } } diff --git a/pkg/amqp-bunny/AmqpProducer.php b/pkg/amqp-bunny/AmqpProducer.php index 753a2d69b..54bc24296 100644 --- a/pkg/amqp-bunny/AmqpProducer.php +++ b/pkg/amqp-bunny/AmqpProducer.php @@ -5,11 +5,13 @@ use Bunny\Channel; use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; +use Interop\Amqp\AmqpDestination as InteropAmqpDestination; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpProducer as InteropAmqpProducer; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; use Interop\Amqp\AmqpTopic as InteropAmqpTopic; use Interop\Queue\DeliveryDelayNotSupportedException; +use Interop\Queue\Exception; use Interop\Queue\InvalidDestinationException; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrDestination; @@ -56,6 +58,8 @@ public function __construct(Channel $channel, AmqpContext $context) } /** + * {@inheritdoc} + * * @param InteropAmqpTopic|InteropAmqpQueue $destination * @param InteropAmqpMessage $message */ @@ -63,48 +67,15 @@ public function send(PsrDestination $destination, PsrMessage $message) { $destination instanceof PsrTopic ? InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpTopic::class) - : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class); + : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class) + ; InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); - if (null !== $this->priority && null === $message->getPriority()) { - $message->setPriority($this->priority); - } - - if (null !== $this->timeToLive && null === $message->getExpiration()) { - $message->setExpiration($this->timeToLive); - } - - $amqpProperties = $message->getHeaders(); - - if (array_key_exists('timestamp', $amqpProperties)) { - $amqpProperties['timestamp'] = \DateTime::createFromFormat('U', $amqpProperties['timestamp']); - } - - if ($appProperties = $message->getProperties()) { - $amqpProperties['application_headers'] = $appProperties; - } - - if ($this->deliveryDelay) { - $this->delayStrategy->delayMessage($this->context, $destination, $message, $this->deliveryDelay); - } elseif ($destination instanceof InteropAmqpTopic) { - $this->channel->publish( - $message->getBody(), - $amqpProperties, - $destination->getTopicName(), - $message->getRoutingKey(), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) - ); - } else { - $this->channel->publish( - $message->getBody(), - $amqpProperties, - '', - $destination->getQueueName(), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) - ); + try { + $this->doSend($destination, $message); + } catch (\Exception $e) { + throw new Exception($e->getMessage(), $e->getCode(), $e); } } @@ -163,4 +134,47 @@ public function getTimeToLive() { return $this->timeToLive; } + + private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage $message) + { + if (null !== $this->priority && null === $message->getPriority()) { + $message->setPriority($this->priority); + } + + if (null !== $this->timeToLive && null === $message->getExpiration()) { + $message->setExpiration($this->timeToLive); + } + + $amqpProperties = $message->getHeaders(); + + if (array_key_exists('timestamp', $amqpProperties) && null !== $amqpProperties['timestamp']) { + $amqpProperties['timestamp'] = \DateTime::createFromFormat('U', $amqpProperties['timestamp']); + } + + if ($appProperties = $message->getProperties()) { + $amqpProperties['application_headers'] = $appProperties; + } + + if ($this->deliveryDelay) { + $this->delayStrategy->delayMessage($this->context, $destination, $message, $this->deliveryDelay); + } elseif ($destination instanceof InteropAmqpTopic) { + $this->channel->publish( + $message->getBody(), + $amqpProperties, + $destination->getTopicName(), + $message->getRoutingKey(), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) + ); + } else { + $this->channel->publish( + $message->getBody(), + $amqpProperties, + '', + $destination->getQueueName(), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) + ); + } + } } diff --git a/pkg/amqp-bunny/BunnyClient.php b/pkg/amqp-bunny/BunnyClient.php new file mode 100644 index 000000000..8697b9de4 --- /dev/null +++ b/pkg/amqp-bunny/BunnyClient.php @@ -0,0 +1,20 @@ +getMessage()) { + throw $e; + } + } + } +} diff --git a/pkg/amqp-bunny/Symfony/AmqpBunnyTransportFactory.php b/pkg/amqp-bunny/Symfony/AmqpBunnyTransportFactory.php deleted file mode 100644 index f83202839..000000000 --- a/pkg/amqp-bunny/Symfony/AmqpBunnyTransportFactory.php +++ /dev/null @@ -1,140 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The connection to AMQP broker set as a string. Other parameters are ignored if set') - ->end() - ->scalarNode('host') - ->defaultValue('localhost') - ->cannotBeEmpty() - ->info('The host to connect too. Note: Max 1024 characters') - ->end() - ->scalarNode('port') - ->defaultValue(5672) - ->cannotBeEmpty() - ->info('Port on the host.') - ->end() - ->scalarNode('user') - ->defaultValue('guest') - ->cannotBeEmpty() - ->info('The user name to use. Note: Max 128 characters.') - ->end() - ->scalarNode('pass') - ->defaultValue('guest') - ->cannotBeEmpty() - ->info('Password. Note: Max 128 characters.') - ->end() - ->scalarNode('vhost') - ->defaultValue('/') - ->cannotBeEmpty() - ->info('The virtual host on the host. Note: Max 128 characters.') - ->end() - ->booleanNode('lazy') - ->defaultTrue() - ->end() - ->enumNode('receive_method') - ->values(['basic_get', 'basic_consume']) - ->defaultValue('basic_get') - ->info('The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher') - ->end() - ->integerNode('heartbeat') - ->defaultValue(0) - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factory = new Definition(AmqpConnectionFactory::class); - $factory->setArguments(isset($config['dsn']) ? [$config['dsn']] : [$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(AmqpContext::class); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(AmqpDriver::class); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/amqp-bunny/Symfony/RabbitMqAmqpBunnyTransportFactory.php b/pkg/amqp-bunny/Symfony/RabbitMqAmqpBunnyTransportFactory.php deleted file mode 100644 index 95341671f..000000000 --- a/pkg/amqp-bunny/Symfony/RabbitMqAmqpBunnyTransportFactory.php +++ /dev/null @@ -1,68 +0,0 @@ -children() - ->scalarNode('delay_strategy') - ->defaultValue('dlx') - ->info('The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = parent::createConnectionFactory($container, $config); - - $this->registerDelayStrategy($container, $config, $factoryId, $this->getName()); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(RabbitMqDriver::class); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } -} diff --git a/pkg/amqp-bunny/Tests/AmqpConnectionFactoryConfigTest.php b/pkg/amqp-bunny/Tests/AmqpConnectionFactoryConfigTest.php deleted file mode 100644 index 4f761fd57..000000000 --- a/pkg/amqp-bunny/Tests/AmqpConnectionFactoryConfigTest.php +++ /dev/null @@ -1,252 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); - - new AmqpConnectionFactory(new \stdClass()); - } - - public function testThrowIfSchemeIsNotAmqp() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be "amqp" only.'); - - new AmqpConnectionFactory('http://example.com'); - } - - public function testThrowIfDsnCouldNotBeParsed() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "amqp://:@/"'); - - new AmqpConnectionFactory('amqp://:@/'); - } - - public function testThrowIfReceiveMenthodIsInvalid() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Invalid "receive_method" option value "invalidMethod". It could be only "basic_get", "basic_consume"'); - - new AmqpConnectionFactory(['receive_method' => 'invalidMethod']); - } - - /** - * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig - */ - public function testShouldParseConfigurationAsExpected($config, $expectedConfig) - { - $factory = new AmqpConnectionFactory($config); - - $this->assertAttributeEquals($expectedConfig, 'config', $factory); - } - - public static function provideConfigs() - { - yield [ - null, - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - // some examples from Appendix A: Examples (https://www.rabbitmq.com/uri-spec.html) - - yield [ - 'amqp+bunny:', - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - 'amqp+bunny://user:pass@host:10000/vhost', - [ - 'host' => 'host', - 'port' => '10000', - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost', - [ - 'host' => 'host', - 'port' => '10000', - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - 'amqp://user%61:%61pass@ho%61st:10000/v%2fhost', - [ - 'host' => 'hoast', - 'port' => '10000', - 'vhost' => 'v/host', - 'user' => 'usera', - 'pass' => 'apass', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - 'amqp://', - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost?qos_prefetch_count=2&qos_prefetch_size=1', - [ - 'host' => 'host', - 'port' => '10000', - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => '1', - 'qos_prefetch_count' => '2', - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - [], - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - ['qos_global' => true, 'host' => 'host'], - [ - 'host' => 'host', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => true, - 'lazy' => true, - ], - ]; - - yield [ - ['qos_prefetch_count' => 123, 'qos_prefetch_size' => 321], - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 321, - 'qos_prefetch_count' => 123, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost?qos_prefetch_size=123&qos_prefetch_count=321', - [ - 'host' => 'host', - 'port' => '10000', - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - 'qos_prefetch_size' => 123, - 'qos_prefetch_count' => 321, - 'qos_global' => false, - 'lazy' => true, - ], - ]; - } -} diff --git a/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php b/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php new file mode 100644 index 000000000..c4e6a1e64 --- /dev/null +++ b/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php @@ -0,0 +1,28 @@ +assertClassImplements(PsrConnectionFactory::class, AmqpConnectionFactory::class); + } + + public function testShouldSupportAmqpLibScheme() + { + // no exception here + new AmqpConnectionFactory('amqp+bunny:'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN scheme "amqp+foo" is not supported. Could be one of "amqp", "amqp+bunny" only.'); + new AmqpConnectionFactory('amqp+foo:'); + } +} diff --git a/pkg/amqp-bunny/Tests/AmqpConsumerTest.php b/pkg/amqp-bunny/Tests/AmqpConsumerTest.php index bc716174a..938dff5d2 100644 --- a/pkg/amqp-bunny/Tests/AmqpConsumerTest.php +++ b/pkg/amqp-bunny/Tests/AmqpConsumerTest.php @@ -5,8 +5,8 @@ use Bunny\Channel; use Bunny\Client; use Bunny\Message; -use Bunny\Protocol\MethodBasicConsumeOkFrame; use Enqueue\AmqpBunny\AmqpConsumer; +use Enqueue\AmqpBunny\AmqpContext; use Enqueue\AmqpBunny\Buffer; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; @@ -30,7 +30,7 @@ public function testShouldImplementConsumerInterface() public function testCouldBeConstructedWithContextAndQueueAndBufferAsArguments() { new AmqpConsumer( - $this->createChannelMock(), + $this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get' @@ -41,14 +41,14 @@ public function testShouldReturnQueue() { $queue = new AmqpQueue('aName'); - $consumer = new AmqpConsumer($this->createChannelMock(), $queue, new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), $queue, new Buffer(), 'basic_get'); $this->assertSame($queue, $consumer->getQueue()); } public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createChannelMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -58,7 +58,7 @@ public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() public function testOnRejectShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createChannelMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -68,78 +68,78 @@ public function testOnRejectShouldThrowExceptionIfNotAmqpMessage() public function testOnAcknowledgeShouldAcknowledgeMessage() { - $bunnyMessage = new Message('', 'delivery-tag', true, '', '', [], 'body'); - - $channel = $this->createChannelMock(); - $channel - ->expects($this->once()) - ->method('get') - ->willReturn($bunnyMessage) - ; + $channel = $this->createBunnyChannelMock(); $channel ->expects($this->once()) ->method('ack') - ->with($this->identicalTo($bunnyMessage)) - ; + ->with($this->isInstanceOf(Message::class)) + ->willReturnCallback(function (Message $message) { + $this->assertSame('theDeliveryTag', $message->deliveryTag); + }); - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getBunnyChannel') + ->willReturn($channel) + ; - $message = $consumer->receiveNoWait(); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - // guard - $this->assertSame('delivery-tag', $message->getDeliveryTag()); + $message = new AmqpMessage(); + $message->setDeliveryTag('theDeliveryTag'); $consumer->acknowledge($message); } public function testOnRejectShouldRejectMessage() { - $bunnyMessage = new Message('', 'delivery-tag', true, '', '', [], 'body'); - - $channel = $this->createChannelMock(); - $channel - ->expects($this->once()) - ->method('get') - ->willReturn($bunnyMessage) - ; + $channel = $this->createBunnyChannelMock(); $channel ->expects($this->once()) ->method('reject') - ->with($this->identicalTo($bunnyMessage), $this->isFalse()) - ; + ->with($this->isInstanceOf(Message::class), false) + ->willReturnCallback(function (Message $message) { + $this->assertSame('theDeliveryTag', $message->deliveryTag); + }); - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getBunnyChannel') + ->willReturn($channel) + ; - $message = $consumer->receiveNoWait(); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - // guard - $this->assertSame('delivery-tag', $message->getDeliveryTag()); + $message = new AmqpMessage(); + $message->setDeliveryTag('theDeliveryTag'); $consumer->reject($message, false); } public function testOnRejectShouldRequeueMessage() { - $bunnyMessage = new Message('', 'delivery-tag', true, '', '', [], 'body'); - - $channel = $this->createChannelMock(); - $channel - ->expects($this->once()) - ->method('get') - ->willReturn($bunnyMessage) - ; + $channel = $this->createBunnyChannelMock(); $channel ->expects($this->once()) ->method('reject') - ->with($this->identicalTo($bunnyMessage), $this->isTrue()) - ; + ->with($this->isInstanceOf(Message::class), true) + ->willReturnCallback(function (Message $message) { + $this->assertSame('theDeliveryTag', $message->deliveryTag); + }); - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getBunnyChannel') + ->willReturn($channel) + ; - $message = $consumer->receiveNoWait(); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - // guard - $this->assertSame('delivery-tag', $message->getDeliveryTag()); + $message = new AmqpMessage(); + $message->setDeliveryTag('theDeliveryTag'); $consumer->reject($message, true); } @@ -148,80 +148,66 @@ public function testShouldReturnMessageOnReceiveNoWait() { $bunnyMessage = new Message('', 'delivery-tag', true, '', '', [], 'body'); - $channel = $this->createChannelMock(); + $message = new AmqpMessage(); + + $channel = $this->createBunnyChannelMock(); $channel ->expects($this->once()) ->method('get') ->willReturn($bunnyMessage) ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getBunnyChannel') + ->willReturn($channel) + ; + $context + ->expects($this->once()) + ->method('convertMessage') + ->with($this->identicalTo($bunnyMessage)) + ->willReturn($message) + ; - $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - $message = $consumer->receiveNoWait(); + $receivedMessage = $consumer->receiveNoWait(); - $this->assertInstanceOf(AmqpMessage::class, $message); - $this->assertSame('body', $message->getBody()); - $this->assertSame('delivery-tag', $message->getDeliveryTag()); - $this->assertTrue($message->isRedelivered()); + $this->assertSame($message, $receivedMessage); } public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() { $bunnyMessage = new Message('', 'delivery-tag', true, '', '', [], 'body'); - $channel = $this->createChannelMock(); + $message = new AmqpMessage(); + + $channel = $this->createBunnyChannelMock(); $channel ->expects($this->once()) ->method('get') ->willReturn($bunnyMessage) ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - - $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); - - $message = $consumer->receive(); - - $this->assertInstanceOf(AmqpMessage::class, $message); - $this->assertSame('body', $message->getBody()); - $this->assertSame('delivery-tag', $message->getDeliveryTag()); - $this->assertTrue($message->isRedelivered()); - } - - public function testShouldCallExpectedMethodsWhenReceiveWithBasicConsumeMethod() - { - $frame = new MethodBasicConsumeOkFrame(); - $frame->consumerTag = 'theConsumerTag'; - - $client = $this->createClientMock(); - $client - ->expects($this->atLeastOnce()) - ->method('run') - ; - - $channel = $this->createChannelMock(); - $channel + $context = $this->createContextMock(); + $context ->expects($this->once()) - ->method('consume') - ->willReturn($frame) + ->method('getBunnyChannel') + ->willReturn($channel) ; - $channel - ->expects($this->atLeastOnce()) - ->method('getClient') - ->willReturn($client) + $context + ->expects($this->once()) + ->method('convertMessage') + ->with($this->identicalTo($bunnyMessage)) + ->willReturn($message) ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_consume'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); - $consumer->receive(1234); + $receivedMessage = $consumer->receive(); - $this->assertSame('theConsumerTag', $consumer->getConsumerTag()); + $this->assertSame($message, $receivedMessage); } /** @@ -232,10 +218,18 @@ public function createClientMock() return $this->createMock(Client::class); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + */ + public function createContextMock() + { + return $this->createMock(AmqpContext::class); + } + /** * @return \PHPUnit_Framework_MockObject_MockObject|Channel */ - public function createChannelMock() + public function createBunnyChannelMock() { return $this->createMock(Channel::class); } diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php new file mode 100644 index 000000000..32820ba6a --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..188118f64 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php new file mode 100644 index 000000000..d2a14f3d9 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php new file mode 100644 index 000000000..eb1a38b0a --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..7d9806f79 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpPreFetchCountTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpPreFetchCountTest.php new file mode 100644 index 000000000..53ae33e14 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpPreFetchCountTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php index c5d7ed40b..1ef23b65b 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php @@ -2,7 +2,8 @@ namespace Enqueue\AmqpBunny\Tests\Spec; -use Enqueue\AmqpLib\AmqpConnectionFactory; +use Enqueue\AmqpBunny\AmqpConnectionFactory; +use Enqueue\AmqpBunny\AmqpContext; use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; @@ -12,6 +13,11 @@ */ class AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest extends SendAndReceiveDelayedMessageFromQueueSpec { + public function test() + { + $this->markTestIncomplete(); + } + /** * {@inheritdoc} */ @@ -24,6 +30,8 @@ protected function createContext() } /** + * @param AmqpContext $context + * * {@inheritdoc} */ protected function createQueue(PsrContext $context, $queueName) @@ -31,6 +39,7 @@ protected function createQueue(PsrContext $context, $queueName) $queue = parent::createQueue($context, $queueName); $context->declareQueue($queue); + $context->purgeQueue($queue); return $queue; } diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php index 7795d01b8..531389a02 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php @@ -2,7 +2,8 @@ namespace Enqueue\AmqpBunny\Tests\Spec; -use Enqueue\AmqpLib\AmqpConnectionFactory; +use Enqueue\AmqpBunny\AmqpConnectionFactory; +use Enqueue\AmqpBunny\AmqpContext; use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; @@ -24,6 +25,8 @@ protected function createContext() } /** + * @param AmqpContext $context + * * {@inheritdoc} */ protected function createQueue(PsrContext $context, $queueName) @@ -31,6 +34,7 @@ protected function createQueue(PsrContext $context, $queueName) $queue = parent::createQueue($context, $queueName); $context->declareQueue($queue); + $context->purgeQueue($queue); return $queue; } diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php new file mode 100644 index 000000000..2bf30f816 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Symfony/AmqpBunnyTransportFactoryTest.php b/pkg/amqp-bunny/Tests/Symfony/AmqpBunnyTransportFactoryTest.php deleted file mode 100644 index 9b2a68bc2..000000000 --- a/pkg/amqp-bunny/Tests/Symfony/AmqpBunnyTransportFactoryTest.php +++ /dev/null @@ -1,218 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, AmqpBunnyTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new AmqpBunnyTransportFactory(); - - $this->assertEquals('amqp_bunny', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new AmqpBunnyTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new AmqpBunnyTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'lazy' => true, - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new AmqpBunnyTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['amqpDSN']); - - $this->assertEquals([ - 'dsn' => 'amqpDSN', - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'lazy' => true, - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - ], $config); - } - - public function testThrowIfInvalidReceiveMethodIsSet() - { - $transport = new AmqpBunnyTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The value "anInvalidMethod" is not allowed for path "foo.receive_method". Permissible values: "basic_get", "basic_consume"'); - $processor->process($tb->buildTree(), [[ - 'receive_method' => 'anInvalidMethod', - ]]); - } - - public function testShouldAllowChangeReceiveMethod() - { - $transport = new AmqpBunnyTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'receive_method' => 'basic_consume', - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'lazy' => true, - 'receive_method' => 'basic_consume', - 'heartbeat' => 0, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new AmqpBunnyTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new AmqpBunnyTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theConnectionDSN', - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame(['theConnectionDSN'], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new AmqpBunnyTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]); - - $this->assertEquals('enqueue.transport.amqp_bunny.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.amqp_bunny.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.amqp_bunny.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new AmqpBunnyTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.amqp_bunny.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(AmqpDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.amqp_bunny.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/amqp-bunny/Tests/Symfony/RabbitMqAmqpBunnyTransportFactoryTest.php b/pkg/amqp-bunny/Tests/Symfony/RabbitMqAmqpBunnyTransportFactoryTest.php deleted file mode 100644 index 555ad777e..000000000 --- a/pkg/amqp-bunny/Tests/Symfony/RabbitMqAmqpBunnyTransportFactoryTest.php +++ /dev/null @@ -1,137 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, RabbitMqAmqpBunnyTransportFactory::class); - } - - public function testShouldExtendAmqpTransportFactoryClass() - { - $this->assertClassExtends(AmqpBunnyTransportFactory::class, RabbitMqAmqpBunnyTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RabbitMqAmqpBunnyTransportFactory(); - - $this->assertEquals('rabbitmq_amqp_bunny', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RabbitMqAmqpBunnyTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RabbitMqAmqpBunnyTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'delay_strategy' => 'dlx', - 'lazy' => true, - 'receive_method' => 'basic_get', - 'heartbeat' => 0, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpBunnyTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpBunnyTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertEquals('enqueue.transport.rabbitmq_amqp_bunny.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.rabbitmq_amqp_bunny.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.rabbitmq_amqp_bunny.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpBunnyTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.rabbitmq_amqp_bunny.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RabbitMqDriver::class, $driver->getClass()); - } -} diff --git a/pkg/amqp-bunny/composer.json b/pkg/amqp-bunny/composer.json index b4e4c495f..9049d5b8a 100644 --- a/pkg/amqp-bunny/composer.json +++ b/pkg/amqp-bunny/composer.json @@ -7,16 +7,16 @@ "require": { "php": ">=5.6", - "queue-interop/amqp-interop": "^0.6@dev", + "queue-interop/amqp-interop": "^0.7@dev", "bunny/bunny": "^0.2.4", - "enqueue/amqp-tools": "^0.7@dev" + "enqueue/amqp-tools": "^0.8@dev" }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/amqp-ext/AmqpConnectionFactory.php b/pkg/amqp-ext/AmqpConnectionFactory.php index 3002ab0db..18de675e6 100644 --- a/pkg/amqp-ext/AmqpConnectionFactory.php +++ b/pkg/amqp-ext/AmqpConnectionFactory.php @@ -2,6 +2,7 @@ namespace Enqueue\AmqpExt; +use Enqueue\AmqpTools\ConnectionConfig; use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; use Interop\Amqp\AmqpConnectionFactory as InteropAmqpConnectionFactory; @@ -11,7 +12,7 @@ class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrate use DelayStrategyAwareTrait; /** - * @var array + * @var ConnectionConfig */ private $config; @@ -21,59 +22,32 @@ class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrate private $connection; /** - * The config could be an array, string DSN or null. In case of null it will attempt to connect to localhost with default credentials. + * @see \Enqueue\AmqpTools\ConnectionConfig for possible config formats and values * - * [ - * 'host' => 'amqp.host The host to connect too. Note: Max 1024 characters.', - * 'port' => 'amqp.port Port on the host.', - * 'vhost' => 'amqp.vhost The virtual host on the host. Note: Max 128 characters.', - * 'user' => 'amqp.user The user name to use. Note: Max 128 characters.', - * 'pass' => 'amqp.password Password. Note: Max 128 characters.', - * 'read_timeout' => 'Timeout in for income activity. Note: 0 or greater seconds. May be fractional.', - * 'write_timeout' => 'Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional.', - * 'connect_timeout' => 'Connection timeout. Note: 0 or greater seconds. May be fractional.', - * 'persisted' => 'bool, Whether it use single persisted connection or open a new one for every context', - * 'lazy' => 'the connection will be performed as later as possible, if the option set to true', - * 'pre_fetch_count' => 'Controls how many messages could be prefetched', - * 'pre_fetch_size' => 'Controls how many messages could be prefetched', - * 'receive_method' => 'Could be either basic_get or basic_consume', - * ] + * In addition this factory accepts next options: + * receive_method - Could be either basic_get or basic_consume * - * or - * - * amqp://user:pass@host:10000/vhost?lazy=true&persisted=false&read_timeout=2 - * - * @param array|string $config + * @param array|string|null $config */ - public function __construct($config = 'amqp://') + public function __construct($config = 'amqp:') { - if (is_string($config) && 0 === strpos($config, 'amqp+ext:')) { - $config = str_replace('amqp+ext:', 'amqp:', $config); - } - - // third argument is deprecated will be removed in 0.8 - if (empty($config) || 'amqp:' === $config || 'amqp://' === $config) { - $config = []; - } elseif (is_string($config)) { - $config = $this->parseDsn($config); - } elseif (is_array($config)) { - } else { - throw new \LogicException('The config must be either an array of options, a DSN string or null'); - } - - $this->config = array_replace($this->defaultConfig(), $config); + $this->config = (new ConnectionConfig($config)) + ->addSupportedScheme('amqp+ext') + ->addDefaultOption('receive_method', 'basic_get') + ->parse() + ; $supportedMethods = ['basic_get', 'basic_consume']; - if (false == in_array($this->config['receive_method'], $supportedMethods, true)) { + if (false == in_array($this->config->getOption('receive_method'), $supportedMethods, true)) { throw new \LogicException(sprintf( 'Invalid "receive_method" option value "%s". It could be only "%s"', - $this->config['receive_method'], + $this->config->getOption('receive_method'), implode('", "', $supportedMethods) )); } - if ('basic_consume' == $this->config['receive_method']) { - if (false == (version_compare(phpversion('amqp'), '1.9.1', '>=') || phpversion('amqp') == '1.9.1-dev')) { + if ('basic_consume' == $this->config->getOption('receive_method')) { + if (false == (version_compare(phpversion('amqp'), '1.9.1', '>=') || '1.9.1-dev' == phpversion('amqp'))) { // @see https://github.com/php-enqueue/enqueue-dev/issues/110 and https://github.com/pdezwart/php-amqp/issues/281 throw new \LogicException('The "basic_consume" method does not work on amqp extension prior 1.9.1 version.'); } @@ -87,21 +61,33 @@ public function __construct($config = 'amqp://') */ public function createContext() { - if ($this->config['lazy']) { + if ($this->config->isLazy()) { $context = new AmqpContext(function () { - return $this->createExtContext($this->establishConnection()); - }, $this->config['receive_method']); + $extContext = $this->createExtContext($this->establishConnection()); + $extContext->qos($this->config->getQosPrefetchSize(), $this->config->getQosPrefetchCount()); + + return $extContext; + }, $this->config->getOption('receive_method')); $context->setDelayStrategy($this->delayStrategy); return $context; } - $context = new AmqpContext($this->createExtContext($this->establishConnection()), $this->config['receive_method']); + $context = new AmqpContext($this->createExtContext($this->establishConnection()), $this->config->getOption('receive_method')); $context->setDelayStrategy($this->delayStrategy); + $context->setQos($this->config->getQosPrefetchSize(), $this->config->getQosPrefetchCount(), $this->config->isQosGlobal()); return $context; } + /** + * @return ConnectionConfig + */ + public function getConfig() + { + return $this->config; + } + /** * @param \AMQPConnection $extConnection * @@ -109,16 +95,7 @@ public function createContext() */ private function createExtContext(\AMQPConnection $extConnection) { - $channel = new \AMQPChannel($extConnection); - if (false == empty($this->config['pre_fetch_count'])) { - $channel->setPrefetchCount((int) $this->config['pre_fetch_count']); - } - - if (false == empty($this->config['pre_fetch_size'])) { - $channel->setPrefetchSize((int) $this->config['pre_fetch_size']); - } - - return $channel; + return new \AMQPChannel($extConnection); } /** @@ -127,85 +104,24 @@ private function createExtContext(\AMQPConnection $extConnection) private function establishConnection() { if (false == $this->connection) { - $config = $this->config; - $config['login'] = $this->config['user']; - $config['password'] = $this->config['pass']; - - $this->connection = new \AMQPConnection($config); - - $this->config['persisted'] ? $this->connection->pconnect() : $this->connection->connect(); + $extConfig = []; + $extConfig['host'] = $this->config->getHost(); + $extConfig['port'] = $this->config->getPort(); + $extConfig['vhost'] = $this->config->getVHost(); + $extConfig['login'] = $this->config->getUser(); + $extConfig['password'] = $this->config->getPass(); + $extConfig['read_timeout'] = $this->config->getReadTimeout(); + $extConfig['write_timeout'] = $this->config->getWriteTimeout(); + $extConfig['connect_timeout'] = $this->config->getConnectionTimeout(); + + $this->connection = new \AMQPConnection($extConfig); + + $this->config->isPersisted() ? $this->connection->pconnect() : $this->connection->connect(); } if (false == $this->connection->isConnected()) { - $this->config['persisted'] ? $this->connection->preconnect() : $this->connection->reconnect(); + $this->config->isPersisted() ? $this->connection->preconnect() : $this->connection->reconnect(); } return $this->connection; } - - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) - { - $dsnConfig = parse_url($dsn); - if (false === $dsnConfig) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); - } - - $dsnConfig = array_replace([ - 'scheme' => null, - 'host' => null, - 'port' => null, - 'user' => null, - 'pass' => null, - 'path' => null, - 'query' => null, - ], $dsnConfig); - - if ('amqp' !== $dsnConfig['scheme']) { - throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be "amqp" only.', $dsnConfig['scheme'])); - } - - if ($dsnConfig['query']) { - $query = []; - parse_str($dsnConfig['query'], $query); - - $dsnConfig = array_replace($query, $dsnConfig); - } - - $dsnConfig['vhost'] = ltrim($dsnConfig['path'], '/'); - - unset($dsnConfig['scheme'], $dsnConfig['query'], $dsnConfig['fragment'], $dsnConfig['path']); - - $config = array_replace($this->defaultConfig(), $dsnConfig); - $config = array_map(function ($value) { - return urldecode($value); - }, $config); - - return $config; - } - - /** - * @return array - */ - private function defaultConfig() - { - return [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ]; - } } diff --git a/pkg/amqp-ext/AmqpConsumer.php b/pkg/amqp-ext/AmqpConsumer.php index 4a3591d37..eb47c0f44 100644 --- a/pkg/amqp-ext/AmqpConsumer.php +++ b/pkg/amqp-ext/AmqpConsumer.php @@ -6,7 +6,6 @@ use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpQueue; use Interop\Amqp\Impl\AmqpMessage; -use Interop\Queue\Exception; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrMessage; @@ -32,11 +31,6 @@ class AmqpConsumer implements InteropAmqpConsumer */ private $extQueue; - /** - * @var bool - */ - private $isInit; - /** * @var string */ @@ -65,8 +59,6 @@ public function __construct(AmqpContext $context, AmqpQueue $queue, Buffer $buff $this->buffer = $buffer; $this->receiveMethod = $receiveMethod; $this->flags = self::FLAG_NOPARAM; - - $this->isInit = false; } /** @@ -74,10 +66,6 @@ public function __construct(AmqpContext $context, AmqpQueue $queue, Buffer $buff */ public function setConsumerTag($consumerTag) { - if ($this->isInit) { - throw new Exception('Consumer tag is not mutable after it has been subscribed to broker'); - } - $this->consumerTag = $consumerTag; } @@ -157,7 +145,7 @@ public function receive($timeout = 0) public function receiveNoWait() { if ($extMessage = $this->getExtQueue()->get(Flags::convertConsumerFlags($this->flags))) { - return $this->convertMessage($extMessage); + return $this->context->convertMessage($extMessage); } } @@ -213,85 +201,44 @@ private function receiveBasicGet($timeout) */ private function receiveBasicConsume($timeout) { - if ($this->isInit && $message = $this->buffer->pop($this->getExtQueue()->getConsumerTag())) { - return $message; + if (false == $this->consumerTag) { + $this->context->subscribe($this, function (InteropAmqpMessage $message) { + $this->buffer->push($message->getConsumerTag(), $message); + + return false; + }); } - /** @var \AMQPQueue $extQueue */ - $extConnection = $this->getExtQueue()->getChannel()->getConnection(); + if ($message = $this->buffer->pop($this->consumerTag)) { + return $message; + } - $originalTimeout = $extConnection->getReadTimeout(); - try { - $extConnection->setReadTimeout($timeout / 1000); + while (true) { + $start = microtime(true); - if (false == $this->isInit) { - $this->getExtQueue()->consume(null, Flags::convertConsumerFlags($this->flags), $this->consumerTag); + $this->context->consume($timeout); - $this->isInit = true; + if ($message = $this->buffer->pop($this->consumerTag)) { + return $message; } - /** @var AmqpMessage|null $message */ - $message = null; - - $this->getExtQueue()->consume(function (\AMQPEnvelope $extEnvelope, \AMQPQueue $q) use (&$message) { - $message = $this->convertMessage($extEnvelope); - $message->setConsumerTag($q->getConsumerTag()); - - if ($this->getExtQueue()->getConsumerTag() == $q->getConsumerTag()) { - return false; - } + // is here when consumed message is not for this consumer - // not our message, put it to buffer and continue. - $this->buffer->push($q->getConsumerTag(), $message); - - $message = null; + // as timeout is infinite have to continue consumption, but it can overflow message buffer + if ($timeout <= 0) { + continue; + } - return true; - }, AMQP_JUST_CONSUME); + // compute remaining timeout and continue until time is up + $stop = microtime(true); + $timeout -= ($stop - $start) * 1000; - return $message; - } catch (\AMQPQueueException $e) { - if ('Consumer timeout exceed' == $e->getMessage()) { - return null; + if ($timeout <= 0) { + break; } - - throw $e; - } finally { - $extConnection->setReadTimeout($originalTimeout); } } - /** - * @param \AMQPEnvelope $extEnvelope - * - * @return AmqpMessage - */ - private function convertMessage(\AMQPEnvelope $extEnvelope) - { - $message = new AmqpMessage( - $extEnvelope->getBody(), - $extEnvelope->getHeaders(), - [ - 'message_id' => $extEnvelope->getMessageId(), - 'correlation_id' => $extEnvelope->getCorrelationId(), - 'app_id' => $extEnvelope->getAppId(), - 'type' => $extEnvelope->getType(), - 'content_encoding' => $extEnvelope->getContentEncoding(), - 'content_type' => $extEnvelope->getContentType(), - 'expiration' => $extEnvelope->getExpiration(), - 'priority' => $extEnvelope->getPriority(), - 'reply_to' => $extEnvelope->getReplyTo(), - 'timestamp' => $extEnvelope->getTimeStamp(), - 'user_id' => $extEnvelope->getUserId(), - ] - ); - $message->setRedelivered($extEnvelope->isRedelivery()); - $message->setDeliveryTag($extEnvelope->getDeliveryTag()); - $message->setRoutingKey($extEnvelope->getRoutingKey()); - - return $message; - } - /** * @return \AMQPQueue */ diff --git a/pkg/amqp-ext/AmqpContext.php b/pkg/amqp-ext/AmqpContext.php index 6d2d4e38f..56262270d 100644 --- a/pkg/amqp-ext/AmqpContext.php +++ b/pkg/amqp-ext/AmqpContext.php @@ -5,6 +5,7 @@ use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; use Interop\Amqp\AmqpBind as InteropAmqpBind; +use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpContext as InteropAmqpContext; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; use Interop\Amqp\AmqpTopic as InteropAmqpTopic; @@ -41,6 +42,13 @@ class AmqpContext implements InteropAmqpContext, DelayStrategyAware */ private $receiveMethod; + /** + * an item contains an array: [AmqpConsumerInterop $consumer, callable $callback];. + * + * @var array + */ + private $subscribers; + /** * Callable must return instance of \AMQPChannel once called. * @@ -60,6 +68,7 @@ public function __construct($extChannel, $receiveMethod) } $this->buffer = new Buffer(); + $this->subscribers = []; } /** @@ -289,4 +298,128 @@ public function getExtChannel() return $this->extChannel; } + + /** + * {@inheritdoc} + */ + public function subscribe(InteropAmqpConsumer $consumer, callable $callback) + { + if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { + return; + } + + $extQueue = new \AMQPQueue($this->getExtChannel()); + $extQueue->setName($consumer->getQueue()->getQueueName()); + + $extQueue->consume(null, Flags::convertConsumerFlags($consumer->getFlags()), $consumer->getConsumerTag()); + + $consumerTag = $extQueue->getConsumerTag(); + $consumer->setConsumerTag($consumerTag); + $this->subscribers[$consumerTag] = [$consumer, $callback]; + } + + /** + * {@inheritdoc} + */ + public function unsubscribe(InteropAmqpConsumer $consumer) + { + if (false == $consumer->getConsumerTag()) { + return; + } + + // seg fault +// $consumerTag = $consumer->getConsumerTag(); +// $consumer->setConsumerTag(null); +// +// $extQueue = new \AMQPQueue($this->getExtChannel()); +// $extQueue->setName($consumer->getQueue()->getQueueName()); +// +// $extQueue->cancel($consumerTag); +// unset($this->subscribers[$consumerTag]); + } + + /** + * {@inheritdoc} + */ + public function consume($timeout = 0) + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + /** @var \AMQPQueue $extQueue */ + $extConnection = $this->getExtChannel()->getConnection(); + + $originalTimeout = $extConnection->getReadTimeout(); + try { + $extConnection->setReadTimeout($timeout / 1000); + + reset($this->subscribers); + /** @var $consumer AmqpConsumer */ + list($consumer) = current($this->subscribers); + + $extQueue = new \AMQPQueue($this->getExtChannel()); + $extQueue->setName($consumer->getQueue()->getQueueName()); + $extQueue->consume(function (\AMQPEnvelope $extEnvelope, \AMQPQueue $q) use ($originalTimeout, $extConnection) { + $consumeTimeout = $extConnection->getReadTimeout(); + try { + $extConnection->setReadTimeout($originalTimeout); + + $message = $this->convertMessage($extEnvelope); + $message->setConsumerTag($q->getConsumerTag()); + + /** + * @var AmqpConsumer + * @var callable $callback + */ + list($consumer, $callback) = $this->subscribers[$q->getConsumerTag()]; + + return call_user_func($callback, $message, $consumer); + } finally { + $extConnection->setReadTimeout($consumeTimeout); + } + }, AMQP_JUST_CONSUME); + } catch (\AMQPQueueException $e) { + if ('Consumer timeout exceed' == $e->getMessage()) { + return null; + } + + throw $e; + } finally { + $extConnection->setReadTimeout($originalTimeout); + } + } + + /** + * @internal It must be used here and in the consumer only + * + * @param \AMQPEnvelope $extEnvelope + * + * @return AmqpMessage + */ + public function convertMessage(\AMQPEnvelope $extEnvelope) + { + $message = new AmqpMessage( + $extEnvelope->getBody(), + $extEnvelope->getHeaders(), + [ + 'message_id' => $extEnvelope->getMessageId(), + 'correlation_id' => $extEnvelope->getCorrelationId(), + 'app_id' => $extEnvelope->getAppId(), + 'type' => $extEnvelope->getType(), + 'content_encoding' => $extEnvelope->getContentEncoding(), + 'content_type' => $extEnvelope->getContentType(), + 'expiration' => $extEnvelope->getExpiration(), + 'priority' => $extEnvelope->getPriority(), + 'reply_to' => $extEnvelope->getReplyTo(), + 'timestamp' => $extEnvelope->getTimeStamp(), + 'user_id' => $extEnvelope->getUserId(), + ] + ); + $message->setRedelivered($extEnvelope->isRedelivery()); + $message->setDeliveryTag($extEnvelope->getDeliveryTag()); + $message->setRoutingKey($extEnvelope->getRoutingKey()); + + return $message; + } } diff --git a/pkg/amqp-ext/AmqpProducer.php b/pkg/amqp-ext/AmqpProducer.php index 456da6bfd..fd649b29c 100644 --- a/pkg/amqp-ext/AmqpProducer.php +++ b/pkg/amqp-ext/AmqpProducer.php @@ -4,11 +4,13 @@ use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; +use Interop\Amqp\AmqpDestination; use Interop\Amqp\AmqpMessage; use Interop\Amqp\AmqpProducer as InteropAmqpProducer; use Interop\Amqp\AmqpQueue; use Interop\Amqp\AmqpTopic; use Interop\Queue\DeliveryDelayNotSupportedException; +use Interop\Queue\Exception; use Interop\Queue\InvalidDestinationException; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrDestination; @@ -64,51 +66,14 @@ public function send(PsrDestination $destination, PsrMessage $message) { $destination instanceof PsrTopic ? InvalidDestinationException::assertDestinationInstanceOf($destination, AmqpTopic::class) - : InvalidDestinationException::assertDestinationInstanceOf($destination, AmqpQueue::class) - ; + : InvalidDestinationException::assertDestinationInstanceOf($destination, AmqpQueue::class); InvalidMessageException::assertMessageInstanceOf($message, AmqpMessage::class); - if (null !== $this->priority && null === $message->getPriority()) { - $message->setPriority($this->priority); - } - - if (null !== $this->timeToLive && null === $message->getExpiration()) { - $message->setExpiration($this->timeToLive); - } - - $amqpAttributes = $message->getHeaders(); - - if ($message->getProperties()) { - $amqpAttributes['headers'] = $message->getProperties(); - } - - if ($this->deliveryDelay) { - $this->delayStrategy->delayMessage($this->context, $destination, $message, $this->deliveryDelay); - } elseif ($destination instanceof AmqpTopic) { - $amqpExchange = new \AMQPExchange($this->amqpChannel); - $amqpExchange->setType($destination->getType()); - $amqpExchange->setName($destination->getTopicName()); - $amqpExchange->setFlags(Flags::convertTopicFlags($destination->getFlags())); - $amqpExchange->setArguments($destination->getArguments()); - - $amqpExchange->publish( - $message->getBody(), - $message->getRoutingKey(), - Flags::convertMessageFlags($message->getFlags()), - $amqpAttributes - ); - } else { - $amqpExchange = new \AMQPExchange($this->amqpChannel); - $amqpExchange->setType(AMQP_EX_TYPE_DIRECT); - $amqpExchange->setName(''); - - $amqpExchange->publish( - $message->getBody(), - $destination->getQueueName(), - Flags::convertMessageFlags($message->getFlags()), - $amqpAttributes - ); + try { + $this->doSend($destination, $message); + } catch (\Exception $e) { + throw new Exception($e->getMessage(), $e->getCode(), $e); } } @@ -167,4 +132,50 @@ public function getTimeToLive() { return $this->timeToLive; } + + private function doSend(AmqpDestination $destination, AmqpMessage $message) + { + if (null !== $this->priority && null === $message->getPriority()) { + $message->setPriority($this->priority); + } + + if (null !== $this->timeToLive && null === $message->getExpiration()) { + $message->setExpiration($this->timeToLive); + } + + $amqpAttributes = $message->getHeaders(); + + if ($message->getProperties()) { + $amqpAttributes['headers'] = $message->getProperties(); + } + + if ($this->deliveryDelay) { + $this->delayStrategy->delayMessage($this->context, $destination, $message, $this->deliveryDelay); + } elseif ($destination instanceof AmqpTopic) { + $amqpExchange = new \AMQPExchange($this->amqpChannel); + $amqpExchange->setType($destination->getType()); + $amqpExchange->setName($destination->getTopicName()); + $amqpExchange->setFlags(Flags::convertTopicFlags($destination->getFlags())); + $amqpExchange->setArguments($destination->getArguments()); + + $amqpExchange->publish( + $message->getBody(), + $message->getRoutingKey(), + Flags::convertMessageFlags($message->getFlags()), + $amqpAttributes + ); + } else { + /** @var AmqpQueue $destination */ + $amqpExchange = new \AMQPExchange($this->amqpChannel); + $amqpExchange->setType(AMQP_EX_TYPE_DIRECT); + $amqpExchange->setName(''); + + $amqpExchange->publish( + $message->getBody(), + $destination->getQueueName(), + Flags::convertMessageFlags($message->getFlags()), + $amqpAttributes + ); + } + } } diff --git a/pkg/amqp-ext/Tests/AmqpConnectionFactoryConfigTest.php b/pkg/amqp-ext/Tests/AmqpConnectionFactoryConfigTest.php deleted file mode 100644 index 99950be4a..000000000 --- a/pkg/amqp-ext/Tests/AmqpConnectionFactoryConfigTest.php +++ /dev/null @@ -1,274 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); - - new AmqpConnectionFactory(new \stdClass()); - } - - public function testThrowIfSchemeIsNotAmqp() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be "amqp" only.'); - - new AmqpConnectionFactory('http://example.com'); - } - - public function testThrowIfDsnCouldNotBeParsed() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "amqp://:@/"'); - - new AmqpConnectionFactory('amqp://:@/'); - } - - public function testThrowIfReceiveMenthodIsInvalid() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Invalid "receive_method" option value "invalidMethod". It could be only "basic_get", "basic_consume"'); - - new AmqpConnectionFactory(['receive_method' => 'invalidMethod']); - } - - /** - * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig - */ - public function testShouldParseConfigurationAsExpected($config, $expectedConfig) - { - $factory = new AmqpConnectionFactory($config); - - $this->assertAttributeEquals($expectedConfig, 'config', $factory); - } - - public static function provideConfigs() - { - yield [ - null, - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - // some examples from Appendix A: Examples (https://www.rabbitmq.com/uri-spec.html) - - yield [ - 'amqp+ext:', - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - 'amqp+ext://user:pass@host:10000/vhost', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - 'amqp://user%61:%61pass@ho%61st:10000/v%2fhost', - [ - 'host' => 'hoast', - 'port' => 10000, - 'vhost' => 'v/host', - 'user' => 'usera', - 'pass' => 'apass', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - 'amqp://', - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost?connect_timeout=2&lazy=', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => '2', - 'persisted' => false, - 'lazy' => '', - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - [], - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - ['lazy' => false, 'host' => 'host'], - [ - 'host' => 'host', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => false, - 'pre_fetch_count' => null, - 'pre_fetch_size' => null, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - ['pre_fetch_count' => 123, 'pre_fetch_size' => 321], - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => 123, - 'pre_fetch_size' => 321, - 'receive_method' => 'basic_get', - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost?pre_fetch_count=123&pre_fetch_size=321', - [ - 'host' => 'host', - 'port' => '10000', - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => null, - 'write_timeout' => null, - 'connect_timeout' => null, - 'persisted' => false, - 'lazy' => true, - 'pre_fetch_count' => 123, - 'pre_fetch_size' => 321, - 'receive_method' => 'basic_get', - ], - ]; - } -} diff --git a/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php b/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php index bc24157d0..251e57439 100644 --- a/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php +++ b/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php @@ -17,6 +17,16 @@ public function testShouldImplementConnectionFactoryInterface() $this->assertClassImplements(PsrConnectionFactory::class, AmqpConnectionFactory::class); } + public function testShouldSupportAmqpExtScheme() + { + // no exception here + new AmqpConnectionFactory('amqp+ext:'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN scheme "amqp+foo" is not supported. Could be one of "amqp", "amqp+ext" only.'); + new AmqpConnectionFactory('amqp+foo:'); + } + public function testShouldCreateLazyContext() { $factory = new AmqpConnectionFactory(['lazy' => true]); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php new file mode 100644 index 000000000..f467f7b66 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..18a4265ee --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php new file mode 100644 index 000000000..9b2112b58 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php new file mode 100644 index 000000000..c5e139f37 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php @@ -0,0 +1,27 @@ +markTestIncomplete('Seg fault.'); + } + + /** + * {@inheritdoc} + */ + protected function createContext() + { + $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); + + return $factory->createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..3d9dcc449 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php @@ -0,0 +1,27 @@ +markTestIncomplete('Seg fault'); + } + + /** + * {@inheritdoc} + */ + protected function createContext() + { + $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); + + return $factory->createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpPreFetchCountTest.php b/pkg/amqp-ext/Tests/Spec/AmqpPreFetchCountTest.php new file mode 100644 index 000000000..265c50e04 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpPreFetchCountTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php index bb1b0c183..b3916896b 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php @@ -2,7 +2,8 @@ namespace Enqueue\AmqpExt\Tests\Spec; -use Enqueue\AmqpLib\AmqpConnectionFactory; +use Enqueue\AmqpExt\AmqpConnectionFactory; +use Enqueue\AmqpExt\AmqpContext; use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; @@ -24,6 +25,8 @@ protected function createContext() } /** + * @param AmqpContext $context + * * {@inheritdoc} */ protected function createQueue(PsrContext $context, $queueName) @@ -31,6 +34,7 @@ protected function createQueue(PsrContext $context, $queueName) $queue = parent::createQueue($context, $queueName); $context->declareQueue($queue); + $context->purgeQueue($queue); return $queue; } diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php index 74d6233f1..251ea07db 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php @@ -2,7 +2,8 @@ namespace Enqueue\AmqpExt\Tests\Spec; -use Enqueue\AmqpLib\AmqpConnectionFactory; +use Enqueue\AmqpExt\AmqpConnectionFactory; +use Enqueue\AmqpExt\AmqpContext; use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; @@ -24,6 +25,8 @@ protected function createContext() } /** + * @param AmqpContext $context + * * {@inheritdoc} */ protected function createQueue(PsrContext $context, $queueName) @@ -31,6 +34,7 @@ protected function createQueue(PsrContext $context, $queueName) $queue = parent::createQueue($context, $queueName); $context->declareQueue($queue); + $context->purgeQueue($queue); return $queue; } diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php new file mode 100644 index 000000000..00d7e3840 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-ext/composer.json b/pkg/amqp-ext/composer.json index b26606d4a..7050a0e26 100644 --- a/pkg/amqp-ext/composer.json +++ b/pkg/amqp-ext/composer.json @@ -8,15 +8,15 @@ "php": ">=5.6", "ext-amqp": "^1.6", - "queue-interop/amqp-interop": "^0.6@dev", - "enqueue/amqp-tools": "^0.7@dev" + "queue-interop/amqp-interop": "^0.7@dev", + "enqueue/amqp-tools": "^0.8@dev" }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "empi89/php-amqp-stubs": "*@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/amqp-lib/AmqpConnectionFactory.php b/pkg/amqp-lib/AmqpConnectionFactory.php index 0ecc1e52c..a44f67146 100644 --- a/pkg/amqp-lib/AmqpConnectionFactory.php +++ b/pkg/amqp-lib/AmqpConnectionFactory.php @@ -2,6 +2,7 @@ namespace Enqueue\AmqpLib; +use Enqueue\AmqpTools\ConnectionConfig; use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; use Interop\Amqp\AmqpConnectionFactory as InteropAmqpConnectionFactory; @@ -16,7 +17,7 @@ class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrate use DelayStrategyAwareTrait; /** - * @var array + * @var ConnectionConfig */ private $config; @@ -26,51 +27,32 @@ class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrate private $connection; /** - * The config could be an array, string DSN or null. In case of null it will attempt to connect to localhost with default credentials. + * @see \Enqueue\AmqpTools\ConnectionConfig for possible config formats and values * - * [ - * 'host' => 'amqp.host The host to connect too. Note: Max 1024 characters.', - * 'port' => 'amqp.port Port on the host.', - * 'vhost' => 'amqp.vhost The virtual host on the host. Note: Max 128 characters.', - * 'user' => 'amqp.user The user name to use. Note: Max 128 characters.', - * 'pass' => 'amqp.password Password. Note: Max 128 characters.', - * 'lazy' => 'the connection will be performed as later as possible, if the option set to true', - * 'stream' => 'stream or socket connection', - * 'receive_method' => 'Could be either basic_get or basic_consume', - * 'qos_prefetch_size' => 'The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit"', - * 'qos_prefetch_count' => 'Specifies a prefetch window in terms of whole messages.', - * 'qos_global' => 'If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection.', - * ] + * In addition this factory accepts next options: + * receive_method - Could be either basic_get or basic_consume * - * or - * - * amqp://user:pass@host:10000/vhost?lazy=true&socket=true - * - * @param array|string $config + * @param array|string|null $config */ - public function __construct($config = 'amqp://') + public function __construct($config = 'amqp:') { - if (is_string($config) && 0 === strpos($config, 'amqp+lib:')) { - $config = str_replace('amqp+lib:', 'amqp:', $config); - } - - // third argument is deprecated will be removed in 0.8 - if (empty($config) || 'amqp:' === $config || 'amqp://' === $config) { - $config = []; - } elseif (is_string($config)) { - $config = $this->parseDsn($config); - } elseif (is_array($config)) { - } else { - throw new \LogicException('The config must be either an array of options, a DSN string or null'); - } - - $this->config = array_replace($this->defaultConfig(), $config); + $this->config = (new ConnectionConfig($config)) + ->addSupportedScheme('amqp+lib') + ->addDefaultOption('stream', true) + ->addDefaultOption('insist', false) + ->addDefaultOption('login_method', 'AMQPLAIN') + ->addDefaultOption('login_response', null) + ->addDefaultOption('locale', 'en_US') + ->addDefaultOption('keepalive', false) + ->addDefaultOption('receive_method', 'basic_get') + ->parse() + ; $supportedMethods = ['basic_get', 'basic_consume']; - if (false == in_array($this->config['receive_method'], $supportedMethods, true)) { + if (false == in_array($this->config->getOption('receive_method'), $supportedMethods, true)) { throw new \LogicException(sprintf( 'Invalid "receive_method" option value "%s". It could be only "%s"', - $this->config['receive_method'], + $this->config->getOption('receive_method'), implode('", "', $supportedMethods) )); } @@ -81,86 +63,94 @@ public function __construct($config = 'amqp://') */ public function createContext() { - $context = new AmqpContext($this->establishConnection(), $this->config); + $context = new AmqpContext($this->establishConnection(), $this->config->getConfig()); $context->setDelayStrategy($this->delayStrategy); return $context; } + /** + * @return ConnectionConfig + */ + public function getConfig() + { + return $this->config; + } + /** * @return AbstractConnection */ private function establishConnection() { if (false == $this->connection) { - if ($this->config['stream']) { - if ($this->config['lazy']) { + if ($this->config->getOption('stream')) { + if ($this->config->isLazy()) { $con = new AMQPLazyConnection( - $this->config['host'], - $this->config['port'], - $this->config['user'], - $this->config['pass'], - $this->config['vhost'], - $this->config['insist'], - $this->config['login_method'], - $this->config['login_response'], - $this->config['locale'], - $this->config['connection_timeout'], - $this->config['read_write_timeout'], + $this->config->getHost(), + $this->config->getPort(), + $this->config->getUser(), + $this->config->getPass(), + $this->config->getVHost(), + $this->config->getOption('insist'), + $this->config->getOption('login_method'), + $this->config->getOption('login_response'), + $this->config->getOption('locale'), + $this->config->getConnectionTimeout(), + (int) round(min($this->config->getReadTimeout(), $this->config->getWriteTimeout())), null, - $this->config['keepalive'], - $this->config['heartbeat'] + $this->config->getOption('keepalive'), + (int) round($this->config->getHeartbeat()) ); } else { $con = new AMQPStreamConnection( - $this->config['host'], - $this->config['port'], - $this->config['user'], - $this->config['pass'], - $this->config['vhost'], - $this->config['insist'], - $this->config['login_method'], - $this->config['login_response'], - $this->config['locale'], - $this->config['connection_timeout'], - $this->config['read_write_timeout'], + $this->config->getHost(), + $this->config->getPort(), + $this->config->getUser(), + $this->config->getPass(), + $this->config->getVHost(), + $this->config->getOption('insist'), + $this->config->getOption('login_method'), + $this->config->getOption('login_response'), + $this->config->getOption('locale'), + $this->config->getConnectionTimeout(), + (int) round(min($this->config->getReadTimeout(), $this->config->getWriteTimeout())), null, - $this->config['keepalive'], - $this->config['heartbeat'] + $this->config->getOption('keepalive'), + (int) round($this->config->getHeartbeat()) ); } } else { - if ($this->config['lazy']) { + if ($this->config->isLazy()) { $con = new AMQPLazySocketConnection( - $this->config['host'], - $this->config['port'], - $this->config['user'], - $this->config['pass'], - $this->config['vhost'], - $this->config['insist'], - $this->config['login_method'], - $this->config['login_response'], - $this->config['locale'], - $this->config['read_timeout'], - $this->config['keepalive'], - $this->config['write_timeout'], - $this->config['heartbeat'] + $this->config->getHost(), + $this->config->getPort(), + $this->config->getUser(), + $this->config->getPass(), + $this->config->getVHost(), + $this->config->getOption('insist'), + $this->config->getOption('login_method'), + $this->config->getOption('login_response'), + $this->config->getOption('locale'), + (int) round($this->config->getReadTimeout()), + $this->config->getOption('keepalive'), + (int) round($this->config->getWriteTimeout()), + (int) round($this->config->getHeartbeat()) ); } else { $con = new AMQPSocketConnection( - $this->config['host'], - $this->config['port'], - $this->config['user'], - $this->config['pass'], - $this->config['vhost'], - $this->config['insist'], - $this->config['login_method'], - $this->config['login_response'], - $this->config['locale'], - $this->config['read_timeout'], - $this->config['keepalive'], - $this->config['write_timeout'], - $this->config['heartbeat'] + $this->config->getHost(), + $this->config->getPort(), + $this->config->getUser(), + $this->config->getPass(), + $this->config->getVHost(), + $this->config->getOption('insist'), + $this->config->getOption('login_method'), + $this->config->getOption('login_response'), + $this->config->getOption('locale'), + (int) round($this->config->getReadTimeout()), + $this->config->getOption('keepalive'), + (int) round($this->config->getWriteTimeout()), + (int) round($this->config->getHeartbeat()) ); } } @@ -170,78 +160,4 @@ private function establishConnection() return $this->connection; } - - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) - { - $dsnConfig = parse_url($dsn); - if (false === $dsnConfig) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); - } - - $dsnConfig = array_replace([ - 'scheme' => null, - 'host' => null, - 'port' => null, - 'user' => null, - 'pass' => null, - 'path' => null, - 'query' => null, - ], $dsnConfig); - - if ('amqp' !== $dsnConfig['scheme']) { - throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be "amqp" only.', $dsnConfig['scheme'])); - } - - if ($dsnConfig['query']) { - $query = []; - parse_str($dsnConfig['query'], $query); - - $dsnConfig = array_replace($query, $dsnConfig); - } - - $dsnConfig['vhost'] = ltrim($dsnConfig['path'], '/'); - - unset($dsnConfig['scheme'], $dsnConfig['query'], $dsnConfig['fragment'], $dsnConfig['path']); - - $dsnConfig = array_map(function ($value) { - return urldecode($value); - }, $dsnConfig); - - return $dsnConfig; - } - - /** - * @return array - */ - private function defaultConfig() - { - return [ - 'stream' => true, - 'lazy' => true, - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'read_timeout' => 3, - 'keepalive' => false, - 'write_timeout' => 3, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'receive_method' => 'basic_get', - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ]; - } } diff --git a/pkg/amqp-lib/AmqpConsumer.php b/pkg/amqp-lib/AmqpConsumer.php index f08289bd9..cd10f39df 100644 --- a/pkg/amqp-lib/AmqpConsumer.php +++ b/pkg/amqp-lib/AmqpConsumer.php @@ -5,17 +5,17 @@ use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; -use Interop\Amqp\Impl\AmqpMessage; -use Interop\Queue\Exception; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrMessage; use PhpAmqpLib\Channel\AMQPChannel; -use PhpAmqpLib\Exception\AMQPTimeoutException; -use PhpAmqpLib\Message\AMQPMessage as LibAMQPMessage; -use PhpAmqpLib\Wire\AMQPTable; class AmqpConsumer implements InteropAmqpConsumer { + /** + * @var AmqpContext + */ + private $context; + /** * @var AMQPChannel */ @@ -31,11 +31,6 @@ class AmqpConsumer implements InteropAmqpConsumer */ private $buffer; - /** - * @var bool - */ - private $isInit; - /** * @var string */ @@ -52,20 +47,19 @@ class AmqpConsumer implements InteropAmqpConsumer private $consumerTag; /** - * @param AMQPChannel $channel + * @param AmqpContext $context * @param InteropAmqpQueue $queue * @param Buffer $buffer * @param string $receiveMethod */ - public function __construct(AMQPChannel $channel, InteropAmqpQueue $queue, Buffer $buffer, $receiveMethod) + public function __construct(AmqpContext $context, InteropAmqpQueue $queue, Buffer $buffer, $receiveMethod) { - $this->channel = $channel; + $this->context = $context; + $this->channel = $context->getLibChannel(); $this->queue = $queue; $this->buffer = $buffer; $this->receiveMethod = $receiveMethod; $this->flags = self::FLAG_NOPARAM; - - $this->isInit = false; } /** @@ -73,10 +67,6 @@ public function __construct(AMQPChannel $channel, InteropAmqpQueue $queue, Buffe */ public function setConsumerTag($consumerTag) { - if ($this->isInit) { - throw new Exception('Consumer tag is not mutable after it has been subscribed to broker'); - } - $this->consumerTag = $consumerTag; } @@ -152,7 +142,7 @@ public function receive($timeout = 0) public function receiveNoWait() { if ($message = $this->channel->basic_get($this->queue->getQueueName(), (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOACK))) { - return $this->convertMessage($message); + return $this->context->convertMessage($message); } } @@ -177,30 +167,6 @@ public function reject(PsrMessage $message, $requeue = false) $this->channel->basic_reject($message->getDeliveryTag(), $requeue); } - /** - * @param LibAMQPMessage $amqpMessage - * - * @return InteropAmqpMessage - */ - private function convertMessage(LibAMQPMessage $amqpMessage) - { - $headers = new AMQPTable($amqpMessage->get_properties()); - $headers = $headers->getNativeData(); - - $properties = []; - if (isset($headers['application_headers'])) { - $properties = $headers['application_headers']; - } - unset($headers['application_headers']); - - $message = new AmqpMessage($amqpMessage->getBody(), $properties, $headers); - $message->setDeliveryTag($amqpMessage->delivery_info['delivery_tag']); - $message->setRedelivered($amqpMessage->delivery_info['redelivered']); - $message->setRoutingKey($amqpMessage->delivery_info['routing_key']); - - return $message; - } - /** * @param int $timeout * @@ -226,63 +192,41 @@ private function receiveBasicGet($timeout) */ private function receiveBasicConsume($timeout) { - if (false === $this->isInit) { - $callback = function (LibAMQPMessage $message) { - $receivedMessage = $this->convertMessage($message); - $receivedMessage->setConsumerTag($message->delivery_info['consumer_tag']); - - $this->buffer->push($receivedMessage->getConsumerTag(), $receivedMessage); - }; - - $consumerTag = $this->channel->basic_consume( - $this->queue->getQueueName(), - $this->getConsumerTag() ?: $this->getQueue()->getConsumerTag(), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOACK), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), - (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT), - $callback - ); - - $this->consumerTag = $consumerTag ?: $this->getQueue()->getConsumerTag(); - - if (empty($this->consumerTag)) { - throw new Exception('Got empty consumer tag'); - } + if (false == $this->consumerTag) { + $this->context->subscribe($this, function (InteropAmqpMessage $message) { + $this->buffer->push($message->getConsumerTag(), $message); - $this->isInit = true; + return false; + }); } if ($message = $this->buffer->pop($this->consumerTag)) { return $message; } - try { - while (true) { - $start = microtime(true); + while (true) { + $start = microtime(true); - $this->channel->wait(null, false, $timeout / 1000); + $this->context->consume($timeout); - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } + if ($message = $this->buffer->pop($this->consumerTag)) { + return $message; + } - // is here when consumed message is not for this consumer + // is here when consumed message is not for this consumer - // as timeout is infinite have to continue consumption, but it can overflow message buffer - if ($timeout <= 0) { - continue; - } + // as timeout is infinite have to continue consumption, but it can overflow message buffer + if ($timeout <= 0) { + continue; + } - // compute remaining timeout and continue until time is up - $stop = microtime(true); - $timeout -= ($stop - $start) * 1000; + // compute remaining timeout and continue until time is up + $stop = microtime(true); + $timeout -= ($stop - $start) * 1000; - if ($timeout <= 0) { - break; - } + if ($timeout <= 0) { + break; } - } catch (AMQPTimeoutException $e) { } } } diff --git a/pkg/amqp-lib/AmqpContext.php b/pkg/amqp-lib/AmqpContext.php index 1a82bb317..d4616c60e 100644 --- a/pkg/amqp-lib/AmqpContext.php +++ b/pkg/amqp-lib/AmqpContext.php @@ -5,6 +5,7 @@ use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; use Interop\Amqp\AmqpBind as InteropAmqpBind; +use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpContext as InteropAmqpContext; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; @@ -19,6 +20,8 @@ use Interop\Queue\PsrTopic; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Connection\AbstractConnection; +use PhpAmqpLib\Exception\AMQPTimeoutException; +use PhpAmqpLib\Message\AMQPMessage as LibAMQPMessage; use PhpAmqpLib\Wire\AMQPTable; class AmqpContext implements InteropAmqpContext, DelayStrategyAware @@ -45,6 +48,13 @@ class AmqpContext implements InteropAmqpContext, DelayStrategyAware */ private $buffer; + /** + * an item contains an array: [AmqpConsumerInterop $consumer, callable $callback];. + * + * @var array + */ + private $subscribers; + /** * @param AbstractConnection $connection * @param array $config @@ -110,10 +120,10 @@ public function createConsumer(PsrDestination $destination) $queue = $this->createTemporaryQueue(); $this->bind(new AmqpBind($destination, $queue, $queue->getQueueName())); - return new AmqpConsumer($this->getChannel(), $queue, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $queue, $this->buffer, $this->config['receive_method']); } - return new AmqpConsumer($this->getChannel(), $destination, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $destination, $this->buffer, $this->config['receive_method']); } /** @@ -121,7 +131,7 @@ public function createConsumer(PsrDestination $destination) */ public function createProducer() { - $producer = new AmqpProducer($this->getChannel(), $this); + $producer = new AmqpProducer($this->getLibChannel(), $this); $producer->setDelayStrategy($this->delayStrategy); return $producer; @@ -132,7 +142,7 @@ public function createProducer() */ public function createTemporaryQueue() { - list($name) = $this->getChannel()->queue_declare('', false, false, true, false); + list($name) = $this->getLibChannel()->queue_declare('', false, false, true, false); $queue = $this->createQueue($name); $queue->addFlag(InteropAmqpQueue::FLAG_EXCLUSIVE); @@ -145,7 +155,7 @@ public function createTemporaryQueue() */ public function declareTopic(InteropAmqpTopic $topic) { - $this->getChannel()->exchange_declare( + $this->getLibChannel()->exchange_declare( $topic->getTopicName(), $topic->getType(), (bool) ($topic->getFlags() & InteropAmqpTopic::FLAG_PASSIVE), @@ -162,7 +172,7 @@ public function declareTopic(InteropAmqpTopic $topic) */ public function deleteTopic(InteropAmqpTopic $topic) { - $this->getChannel()->exchange_delete( + $this->getLibChannel()->exchange_delete( $topic->getTopicName(), (bool) ($topic->getFlags() & InteropAmqpTopic::FLAG_IFUNUSED), (bool) ($topic->getFlags() & InteropAmqpTopic::FLAG_NOWAIT) @@ -174,7 +184,7 @@ public function deleteTopic(InteropAmqpTopic $topic) */ public function declareQueue(InteropAmqpQueue $queue) { - list(, $messageCount) = $this->getChannel()->queue_declare( + list(, $messageCount) = $this->getLibChannel()->queue_declare( $queue->getQueueName(), (bool) ($queue->getFlags() & InteropAmqpQueue::FLAG_PASSIVE), (bool) ($queue->getFlags() & InteropAmqpQueue::FLAG_DURABLE), @@ -192,7 +202,7 @@ public function declareQueue(InteropAmqpQueue $queue) */ public function deleteQueue(InteropAmqpQueue $queue) { - $this->getChannel()->queue_delete( + $this->getLibChannel()->queue_delete( $queue->getQueueName(), (bool) ($queue->getFlags() & InteropAmqpQueue::FLAG_IFUNUSED), (bool) ($queue->getFlags() & InteropAmqpQueue::FLAG_IFEMPTY), @@ -205,7 +215,7 @@ public function deleteQueue(InteropAmqpQueue $queue) */ public function purgeQueue(InteropAmqpQueue $queue) { - $this->getChannel()->queue_purge( + $this->getLibChannel()->queue_purge( $queue->getQueueName(), (bool) ($queue->getFlags() & InteropAmqpQueue::FLAG_NOWAIT) ); @@ -222,7 +232,7 @@ public function bind(InteropAmqpBind $bind) // bind exchange to exchange if ($bind->getSource() instanceof InteropAmqpTopic && $bind->getTarget() instanceof InteropAmqpTopic) { - $this->getChannel()->exchange_bind( + $this->getLibChannel()->exchange_bind( $bind->getTarget()->getTopicName(), $bind->getSource()->getTopicName(), $bind->getRoutingKey(), @@ -231,7 +241,7 @@ public function bind(InteropAmqpBind $bind) ); // bind queue to exchange } elseif ($bind->getSource() instanceof InteropAmqpQueue) { - $this->getChannel()->queue_bind( + $this->getLibChannel()->queue_bind( $bind->getSource()->getQueueName(), $bind->getTarget()->getTopicName(), $bind->getRoutingKey(), @@ -240,7 +250,7 @@ public function bind(InteropAmqpBind $bind) ); // bind exchange to queue } else { - $this->getChannel()->queue_bind( + $this->getLibChannel()->queue_bind( $bind->getTarget()->getQueueName(), $bind->getSource()->getTopicName(), $bind->getRoutingKey(), @@ -261,7 +271,7 @@ public function unbind(InteropAmqpBind $bind) // bind exchange to exchange if ($bind->getSource() instanceof InteropAmqpTopic && $bind->getTarget() instanceof InteropAmqpTopic) { - $this->getChannel()->exchange_unbind( + $this->getLibChannel()->exchange_unbind( $bind->getTarget()->getTopicName(), $bind->getSource()->getTopicName(), $bind->getRoutingKey(), @@ -270,7 +280,7 @@ public function unbind(InteropAmqpBind $bind) ); // bind queue to exchange } elseif ($bind->getSource() instanceof InteropAmqpQueue) { - $this->getChannel()->queue_unbind( + $this->getLibChannel()->queue_unbind( $bind->getSource()->getQueueName(), $bind->getTarget()->getTopicName(), $bind->getRoutingKey(), @@ -278,7 +288,7 @@ public function unbind(InteropAmqpBind $bind) ); // bind exchange to queue } else { - $this->getChannel()->queue_unbind( + $this->getLibChannel()->queue_unbind( $bind->getTarget()->getQueueName(), $bind->getSource()->getTopicName(), $bind->getRoutingKey(), @@ -299,13 +309,105 @@ public function close() */ public function setQos($prefetchSize, $prefetchCount, $global) { - $this->getChannel()->basic_qos($prefetchSize, $prefetchCount, $global); + $this->getLibChannel()->basic_qos($prefetchSize, $prefetchCount, $global); + } + + /** + * {@inheritdoc} + */ + public function subscribe(InteropAmqpConsumer $consumer, callable $callback) + { + if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { + return; + } + + $libCallback = function (LibAMQPMessage $message) { + $receivedMessage = $this->convertMessage($message); + $receivedMessage->setConsumerTag($message->delivery_info['consumer_tag']); + + /** + * @var AmqpConsumer + * @var callable $callback + */ + list($consumer, $callback) = $this->subscribers[$message->delivery_info['consumer_tag']]; + + if (false === call_user_func($callback, $receivedMessage, $consumer)) { + throw new StopBasicConsumptionException(); + } + }; + + $consumerTag = $this->getLibChannel()->basic_consume( + $consumer->getQueue()->getQueueName(), + $consumer->getConsumerTag(), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOACK), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT), + $libCallback + ); + + if (empty($consumerTag)) { + throw new Exception('Got empty consumer tag'); + } + + $consumer->setConsumerTag($consumerTag); + + $this->subscribers[$consumerTag] = [$consumer, $callback]; + } + + /** + * {@inheritdoc} + */ + public function unsubscribe(InteropAmqpConsumer $consumer) + { + if (false == $consumer->getConsumerTag()) { + return; + } + + $consumerTag = $consumer->getConsumerTag(); + + $this->getLibChannel()->basic_cancel($consumerTag); + + $consumer->setConsumerTag(null); + unset($this->subscribers[$consumerTag], $this->getLibChannel()->callbacks[$consumerTag]); + } + + /** + * {@inheritdoc} + */ + public function consume($timeout = 0) + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + try { + while (true) { + $start = microtime(true); + + $this->channel->wait(null, false, $timeout / 1000); + + if ($timeout <= 0) { + continue; + } + + // compute remaining timeout and continue until time is up + $stop = microtime(true); + $timeout -= ($stop - $start) * 1000; + + if ($timeout <= 0) { + break; + } + } + } catch (AMQPTimeoutException $e) { + } catch (StopBasicConsumptionException $e) { + } } /** * @return AMQPChannel */ - private function getChannel() + public function getLibChannel() { if (null === $this->channel) { $this->channel = $this->connection->channel(); @@ -318,4 +420,30 @@ private function getChannel() return $this->channel; } + + /** + * @internal It must be used here and in the consumer only + * + * @param LibAMQPMessage $amqpMessage + * + * @return InteropAmqpMessage + */ + public function convertMessage(LibAMQPMessage $amqpMessage) + { + $headers = new AMQPTable($amqpMessage->get_properties()); + $headers = $headers->getNativeData(); + + $properties = []; + if (isset($headers['application_headers'])) { + $properties = $headers['application_headers']; + } + unset($headers['application_headers']); + + $message = new AmqpMessage($amqpMessage->getBody(), $properties, $headers); + $message->setDeliveryTag($amqpMessage->delivery_info['delivery_tag']); + $message->setRedelivered($amqpMessage->delivery_info['redelivered']); + $message->setRoutingKey($amqpMessage->delivery_info['routing_key']); + + return $message; + } } diff --git a/pkg/amqp-lib/AmqpProducer.php b/pkg/amqp-lib/AmqpProducer.php index 7da4b6acc..05ebdb579 100644 --- a/pkg/amqp-lib/AmqpProducer.php +++ b/pkg/amqp-lib/AmqpProducer.php @@ -4,11 +4,13 @@ use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; +use Interop\Amqp\AmqpDestination as InteropAmqpDestination; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpProducer as InteropAmqpProducer; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; use Interop\Amqp\AmqpTopic as InteropAmqpTopic; use Interop\Queue\DeliveryDelayNotSupportedException; +use Interop\Queue\Exception; use Interop\Queue\InvalidDestinationException; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrDestination; @@ -58,6 +60,8 @@ public function __construct(AMQPChannel $channel, AmqpContext $context) } /** + * {@inheritdoc} + * * @param InteropAmqpTopic|InteropAmqpQueue $destination * @param InteropAmqpMessage $message */ @@ -70,40 +74,10 @@ public function send(PsrDestination $destination, PsrMessage $message) InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); - if (null !== $this->priority && null === $message->getPriority()) { - $message->setPriority($this->priority); - } - - if (null !== $this->timeToLive && null === $message->getExpiration()) { - $message->setExpiration($this->timeToLive); - } - - $amqpProperties = $message->getHeaders(); - - if ($appProperties = $message->getProperties()) { - $amqpProperties['application_headers'] = new AMQPTable($appProperties); - } - - $amqpMessage = new LibAMQPMessage($message->getBody(), $amqpProperties); - - if ($this->deliveryDelay) { - $this->delayStrategy->delayMessage($this->context, $destination, $message, $this->deliveryDelay); - } elseif ($destination instanceof InteropAmqpTopic) { - $this->channel->basic_publish( - $amqpMessage, - $destination->getTopicName(), - $message->getRoutingKey(), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) - ); - } else { - $this->channel->basic_publish( - $amqpMessage, - '', - $destination->getQueueName(), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), - (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) - ); + try { + $this->doSend($destination, $message); + } catch (\Exception $e) { + throw new Exception($e->getMessage(), $e->getCode(), $e); } } @@ -162,4 +136,43 @@ public function getTimeToLive() { return $this->timeToLive; } + + private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage $message) + { + if (null !== $this->priority && null === $message->getPriority()) { + $message->setPriority($this->priority); + } + + if (null !== $this->timeToLive && null === $message->getExpiration()) { + $message->setExpiration($this->timeToLive); + } + + $amqpProperties = $message->getHeaders(); + + if ($appProperties = $message->getProperties()) { + $amqpProperties['application_headers'] = new AMQPTable($appProperties); + } + + $amqpMessage = new LibAMQPMessage($message->getBody(), $amqpProperties); + + if ($this->deliveryDelay) { + $this->delayStrategy->delayMessage($this->context, $destination, $message, $this->deliveryDelay); + } elseif ($destination instanceof InteropAmqpTopic) { + $this->channel->basic_publish( + $amqpMessage, + $destination->getTopicName(), + $message->getRoutingKey(), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) + ); + } else { + $this->channel->basic_publish( + $amqpMessage, + '', + $destination->getQueueName(), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_MANDATORY), + (bool) ($message->getFlags() & InteropAmqpMessage::FLAG_IMMEDIATE) + ); + } + } } diff --git a/pkg/amqp-lib/StopBasicConsumptionException.php b/pkg/amqp-lib/StopBasicConsumptionException.php new file mode 100644 index 000000000..14d6848e0 --- /dev/null +++ b/pkg/amqp-lib/StopBasicConsumptionException.php @@ -0,0 +1,7 @@ +name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The connection to AMQP broker set as a string. Other parameters are ignored if set') - ->end() - ->scalarNode('host') - ->defaultValue('localhost') - ->cannotBeEmpty() - ->info('The host to connect too. Note: Max 1024 characters') - ->end() - ->scalarNode('port') - ->defaultValue(5672) - ->cannotBeEmpty() - ->info('Port on the host.') - ->end() - ->scalarNode('user') - ->defaultValue('guest') - ->cannotBeEmpty() - ->info('The user name to use. Note: Max 128 characters.') - ->end() - ->scalarNode('pass') - ->defaultValue('guest') - ->cannotBeEmpty() - ->info('Password. Note: Max 128 characters.') - ->end() - ->scalarNode('vhost') - ->defaultValue('/') - ->cannotBeEmpty() - ->info('The virtual host on the host. Note: Max 128 characters.') - ->end() - ->integerNode('connection_timeout') - ->defaultValue(3.0) - ->min(0) - ->info('Connection timeout. Note: 0 or greater seconds. May be fractional.') - ->end() - ->integerNode('read_write_timeout') - ->defaultValue(3.0) - ->min(0) - ->end() - ->integerNode('read_timeout') - ->defaultValue(3) - ->min(0) - ->info('Timeout in for income activity. Note: 0 or greater seconds. May be fractional.') - ->end() - ->integerNode('write_timeout') - ->defaultValue(3) - ->min(0) - ->info('Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional.') - ->end() - ->booleanNode('lazy') - ->defaultTrue() - ->end() - ->booleanNode('stream') - ->defaultTrue() - ->end() - ->booleanNode('insist') - ->defaultFalse() - ->end() - ->booleanNode('keepalive') - ->defaultFalse() - ->end() - ->enumNode('receive_method') - ->values(['basic_get', 'basic_consume']) - ->defaultValue('basic_get') - ->info('The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher') - ->end() - ->integerNode('heartbeat') - ->defaultValue(0) - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factory = new Definition(AmqpConnectionFactory::class); - $factory->setArguments(isset($config['dsn']) ? [$config['dsn']] : [$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(AmqpContext::class); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(AmqpDriver::class); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/amqp-lib/Symfony/RabbitMqAmqpLibTransportFactory.php b/pkg/amqp-lib/Symfony/RabbitMqAmqpLibTransportFactory.php deleted file mode 100644 index 43a88a2da..000000000 --- a/pkg/amqp-lib/Symfony/RabbitMqAmqpLibTransportFactory.php +++ /dev/null @@ -1,68 +0,0 @@ -children() - ->scalarNode('delay_strategy') - ->defaultValue('dlx') - ->info('The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = parent::createConnectionFactory($container, $config); - - $this->registerDelayStrategy($container, $config, $factoryId, $this->getName()); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(RabbitMqDriver::class); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } -} diff --git a/pkg/amqp-lib/Tests/AmqpConnectionFactoryConfigTest.php b/pkg/amqp-lib/Tests/AmqpConnectionFactoryConfigTest.php deleted file mode 100644 index 50dcc12cc..000000000 --- a/pkg/amqp-lib/Tests/AmqpConnectionFactoryConfigTest.php +++ /dev/null @@ -1,362 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); - - new AmqpConnectionFactory(new \stdClass()); - } - - public function testThrowIfSchemeIsNotAmqp() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be "amqp" only.'); - - new AmqpConnectionFactory('http://example.com'); - } - - public function testThrowIfDsnCouldNotBeParsed() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "amqp://:@/"'); - - new AmqpConnectionFactory('amqp://:@/'); - } - - public function testThrowIfReceiveMenthodIsInvalid() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Invalid "receive_method" option value "invalidMethod". It could be only "basic_get", "basic_consume"'); - - new AmqpConnectionFactory(['receive_method' => 'invalidMethod']); - } - - /** - * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig - */ - public function testShouldParseConfigurationAsExpected($config, $expectedConfig) - { - $factory = new AmqpConnectionFactory($config); - - $this->assertAttributeEquals($expectedConfig, 'config', $factory); - } - - public static function provideConfigs() - { - yield [ - null, - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - // some examples from Appendix A: Examples (https://www.rabbitmq.com/uri-spec.html) - - yield [ - 'amqp+lib:', - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - 'amqp+lib://user:pass@host:10000/vhost', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - 'amqp://user%61:%61pass@ho%61st:10000/v%2fhost', - [ - 'host' => 'hoast', - 'port' => 10000, - 'vhost' => 'v/host', - 'user' => 'usera', - 'pass' => 'apass', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - 'amqp://', - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost?connection_timeout=2&lazy=', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => '', - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => '2', - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - [], - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - ['lazy' => false, 'host' => 'host'], - [ - 'host' => 'host', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => false, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - ['connection_timeout' => 123, 'read_write_timeout' => 321], - [ - 'host' => 'localhost', - 'port' => 5672, - 'vhost' => '/', - 'user' => 'guest', - 'pass' => 'guest', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => 123, - 'read_write_timeout' => 321, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - - yield [ - 'amqp://user:pass@host:10000/vhost?connection_timeout=123&read_write_timeout=321', - [ - 'host' => 'host', - 'port' => 10000, - 'vhost' => 'vhost', - 'user' => 'user', - 'pass' => 'pass', - 'read_timeout' => 3, - 'write_timeout' => 3, - 'lazy' => true, - 'receive_method' => 'basic_get', - 'stream' => true, - 'insist' => false, - 'login_method' => 'AMQPLAIN', - 'login_response' => null, - 'locale' => 'en_US', - 'keepalive' => false, - 'heartbeat' => 0, - 'connection_timeout' => '123', - 'read_write_timeout' => '321', - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'qos_global' => false, - ], - ]; - } -} diff --git a/pkg/amqp-lib/Tests/AmqpConnectionFactoryTest.php b/pkg/amqp-lib/Tests/AmqpConnectionFactoryTest.php new file mode 100644 index 000000000..4bcf8f156 --- /dev/null +++ b/pkg/amqp-lib/Tests/AmqpConnectionFactoryTest.php @@ -0,0 +1,28 @@ +assertClassImplements(PsrConnectionFactory::class, AmqpConnectionFactory::class); + } + + public function testShouldSupportAmqpLibScheme() + { + // no exception here + new AmqpConnectionFactory('amqp+lib:'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN scheme "amqp+foo" is not supported. Could be one of "amqp", "amqp+lib" only.'); + new AmqpConnectionFactory('amqp+foo:'); + } +} diff --git a/pkg/amqp-lib/Tests/AmqpConsumerTest.php b/pkg/amqp-lib/Tests/AmqpConsumerTest.php index 46bb138e2..52d083ef1 100644 --- a/pkg/amqp-lib/Tests/AmqpConsumerTest.php +++ b/pkg/amqp-lib/Tests/AmqpConsumerTest.php @@ -3,6 +3,7 @@ namespace Enqueue\AmqpLib\Tests; use Enqueue\AmqpLib\AmqpConsumer; +use Enqueue\AmqpLib\AmqpContext; use Enqueue\AmqpLib\Buffer; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; @@ -27,7 +28,7 @@ public function testShouldImplementConsumerInterface() public function testCouldBeConstructedWithContextAndQueueAndBufferAsArguments() { new AmqpConsumer( - $this->createChannelMock(), + $this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get' @@ -38,14 +39,14 @@ public function testShouldReturnQueue() { $queue = new AmqpQueue('aName'); - $consumer = new AmqpConsumer($this->createChannelMock(), $queue, new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), $queue, new Buffer(), 'basic_get'); $this->assertSame($queue, $consumer->getQueue()); } public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createChannelMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -55,7 +56,7 @@ public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() public function testOnRejectShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createChannelMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -65,14 +66,21 @@ public function testOnRejectShouldThrowExceptionIfNotAmqpMessage() public function testOnAcknowledgeShouldAcknowledgeMessage() { - $channel = $this->createChannelMock(); + $channel = $this->createLibChannelMock(); $channel ->expects($this->once()) ->method('basic_ack') ->with('delivery-tag') ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getLibChannel') + ->willReturn($channel) + ; + + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); $message = new AmqpMessage(); $message->setDeliveryTag('delivery-tag'); @@ -82,14 +90,21 @@ public function testOnAcknowledgeShouldAcknowledgeMessage() public function testOnRejectShouldRejectMessage() { - $channel = $this->createChannelMock(); + $channel = $this->createLibChannelMock(); $channel ->expects($this->once()) ->method('basic_reject') ->with('delivery-tag', $this->isTrue()) ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getLibChannel') + ->willReturn($channel) + ; + + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); $message = new AmqpMessage(); $message->setDeliveryTag('delivery-tag'); @@ -99,87 +114,89 @@ public function testOnRejectShouldRejectMessage() public function testShouldReturnMessageOnReceiveNoWait() { - $amqpMessage = new \PhpAmqpLib\Message\AMQPMessage('body'); - $amqpMessage->delivery_info['delivery_tag'] = 'delivery-tag'; - $amqpMessage->delivery_info['routing_key'] = 'routing-key'; - $amqpMessage->delivery_info['redelivered'] = true; - $amqpMessage->delivery_info['routing_key'] = 'routing-key'; + $libMessage = new \PhpAmqpLib\Message\AMQPMessage('body'); + $libMessage->delivery_info['delivery_tag'] = 'delivery-tag'; + $libMessage->delivery_info['routing_key'] = 'routing-key'; + $libMessage->delivery_info['redelivered'] = true; + $libMessage->delivery_info['routing_key'] = 'routing-key'; + + $message = new AmqpMessage(); - $channel = $this->createChannelMock(); + $channel = $this->createLibChannelMock(); $channel ->expects($this->once()) ->method('basic_get') - ->willReturn($amqpMessage) + ->willReturn($libMessage) ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getLibChannel') + ->willReturn($channel) + ; + $context + ->expects($this->once()) + ->method('convertMessage') + ->with($this->identicalTo($libMessage)) + ->willReturn($message) + ; - $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - $message = $consumer->receiveNoWait(); + $receivedMessage = $consumer->receiveNoWait(); - $this->assertInstanceOf(AmqpMessage::class, $message); - $this->assertSame('body', $message->getBody()); - $this->assertSame('delivery-tag', $message->getDeliveryTag()); - $this->assertSame('routing-key', $message->getRoutingKey()); - $this->assertTrue($message->isRedelivered()); + $this->assertSame($message, $receivedMessage); } public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() { - $amqpMessage = new \PhpAmqpLib\Message\AMQPMessage('body'); - $amqpMessage->delivery_info['delivery_tag'] = 'delivery-tag'; - $amqpMessage->delivery_info['routing_key'] = 'routing-key'; - $amqpMessage->delivery_info['redelivered'] = true; + $libMessage = new \PhpAmqpLib\Message\AMQPMessage('body'); + $libMessage->delivery_info['delivery_tag'] = 'delivery-tag'; + $libMessage->delivery_info['routing_key'] = 'routing-key'; + $libMessage->delivery_info['redelivered'] = true; + + $message = new AmqpMessage(); - $channel = $this->createChannelMock(); + $channel = $this->createLibChannelMock(); $channel ->expects($this->once()) ->method('basic_get') - ->willReturn($amqpMessage) + ->willReturn($libMessage) ; - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getLibChannel') + ->willReturn($channel) + ; + $context + ->expects($this->once()) + ->method('convertMessage') + ->with($this->identicalTo($libMessage)) + ->willReturn($message) + ; - $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); - $message = $consumer->receive(); + $receivedMessage = $consumer->receive(); - $this->assertInstanceOf(AmqpMessage::class, $message); - $this->assertSame('body', $message->getBody()); - $this->assertSame('delivery-tag', $message->getDeliveryTag()); - $this->assertSame('routing-key', $message->getRoutingKey()); - $this->assertTrue($message->isRedelivered()); + $this->assertSame($message, $receivedMessage); } - public function testShouldCallExpectedMethodsWhenReceiveWithBasicConsumeMethod() + /** + * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + */ + public function createContextMock() { - $channel = $this->createChannelMock(); - $channel - ->expects($this->once()) - ->method('basic_consume') - ->willReturn('consumer-tag') - ; - $channel - ->expects($this->once()) - ->method('wait') - ->willReturnCallback(function () { - usleep(2000); - }); - - $consumer = new AmqpConsumer($channel, new AmqpQueue('aName'), new Buffer(), 'basic_consume'); - - $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); - $consumer->receive(1); + return $this->createMock(AmqpContext::class); } /** * @return \PHPUnit_Framework_MockObject_MockObject|AMQPChannel */ - public function createChannelMock() + public function createLibChannelMock() { return $this->createMock(AMQPChannel::class); } diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php new file mode 100644 index 000000000..2d77ff55d --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..5eb07345a --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php new file mode 100644 index 000000000..1a5c939d0 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php new file mode 100644 index 000000000..caeac1f61 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..c751b2c3e --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php b/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php new file mode 100644 index 000000000..9285d598f --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php @@ -0,0 +1,22 @@ +createContext()->createProducer(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php index 12f93a4f1..42bc523ce 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php @@ -3,6 +3,7 @@ namespace Enqueue\AmqpLib\Tests\Spec; use Enqueue\AmqpLib\AmqpConnectionFactory; +use Enqueue\AmqpLib\AmqpContext; use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; @@ -24,6 +25,8 @@ protected function createContext() } /** + * @param AmqpContext $context + * * {@inheritdoc} */ protected function createQueue(PsrContext $context, $queueName) @@ -31,6 +34,7 @@ protected function createQueue(PsrContext $context, $queueName) $queue = parent::createQueue($context, $queueName); $context->declareQueue($queue); + $context->purgeQueue($queue); return $queue; } diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php index ca2284443..e27166695 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php @@ -3,6 +3,7 @@ namespace Enqueue\AmqpLib\Tests\Spec; use Enqueue\AmqpLib\AmqpConnectionFactory; +use Enqueue\AmqpLib\AmqpContext; use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; @@ -24,6 +25,8 @@ protected function createContext() } /** + * @param AmqpContext $context + * * {@inheritdoc} */ protected function createQueue(PsrContext $context, $queueName) @@ -31,6 +34,7 @@ protected function createQueue(PsrContext $context, $queueName) $queue = parent::createQueue($context, $queueName); $context->declareQueue($queue); + $context->purgeQueue($queue); return $queue; } diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php new file mode 100644 index 000000000..2574f5ab2 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php @@ -0,0 +1,22 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Symfony/AmqpLibTransportFactoryTest.php b/pkg/amqp-lib/Tests/Symfony/AmqpLibTransportFactoryTest.php deleted file mode 100644 index 3aaf3bc6d..000000000 --- a/pkg/amqp-lib/Tests/Symfony/AmqpLibTransportFactoryTest.php +++ /dev/null @@ -1,239 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, AmqpLibTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new AmqpLibTransportFactory(); - - $this->assertEquals('amqp_lib', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new AmqpLibTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new AmqpLibTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'lazy' => true, - 'receive_method' => 'basic_get', - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'read_timeout' => 3, - 'write_timeout' => 3, - 'stream' => true, - 'insist' => false, - 'keepalive' => false, - 'heartbeat' => 0, - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new AmqpLibTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['amqpDSN']); - - $this->assertEquals([ - 'dsn' => 'amqpDSN', - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'lazy' => true, - 'receive_method' => 'basic_get', - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'read_timeout' => 3, - 'write_timeout' => 3, - 'stream' => true, - 'insist' => false, - 'keepalive' => false, - 'heartbeat' => 0, - ], $config); - } - - public function testThrowIfInvalidReceiveMethodIsSet() - { - $transport = new AmqpLibTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The value "anInvalidMethod" is not allowed for path "foo.receive_method". Permissible values: "basic_get", "basic_consume"'); - $processor->process($tb->buildTree(), [[ - 'receive_method' => 'anInvalidMethod', - ]]); - } - - public function testShouldAllowChangeReceiveMethod() - { - $transport = new AmqpLibTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'receive_method' => 'basic_consume', - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'lazy' => true, - 'receive_method' => 'basic_consume', - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'read_timeout' => 3, - 'write_timeout' => 3, - 'stream' => true, - 'insist' => false, - 'keepalive' => false, - 'heartbeat' => 0, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new AmqpLibTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new AmqpLibTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theConnectionDSN', - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame(['theConnectionDSN'], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new AmqpLibTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - ]); - - $this->assertEquals('enqueue.transport.amqp_lib.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.amqp_lib.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.amqp_lib.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new AmqpLibTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.amqp_lib.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(AmqpDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.amqp_lib.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/amqp-lib/Tests/Symfony/RabbitMqAmqpLibTransportFactoryTest.php b/pkg/amqp-lib/Tests/Symfony/RabbitMqAmqpLibTransportFactoryTest.php deleted file mode 100644 index b86a57bdf..000000000 --- a/pkg/amqp-lib/Tests/Symfony/RabbitMqAmqpLibTransportFactoryTest.php +++ /dev/null @@ -1,144 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, RabbitMqAmqpLibTransportFactory::class); - } - - public function testShouldExtendAmqpTransportFactoryClass() - { - $this->assertClassExtends(AmqpLibTransportFactory::class, RabbitMqAmqpLibTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RabbitMqAmqpLibTransportFactory(); - - $this->assertEquals('rabbitmq_amqp_lib', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RabbitMqAmqpLibTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RabbitMqAmqpLibTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'delay_strategy' => 'dlx', - 'lazy' => true, - 'receive_method' => 'basic_get', - 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, - 'read_timeout' => 3, - 'write_timeout' => 3, - 'stream' => true, - 'insist' => false, - 'keepalive' => false, - 'heartbeat' => 0, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpLibTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpLibTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertEquals('enqueue.transport.rabbitmq_amqp_lib.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.rabbitmq_amqp_lib.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.rabbitmq_amqp_lib.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpLibTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.rabbitmq_amqp_lib.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RabbitMqDriver::class, $driver->getClass()); - } -} diff --git a/pkg/amqp-lib/composer.json b/pkg/amqp-lib/composer.json index d530b6567..7d3f2b959 100644 --- a/pkg/amqp-lib/composer.json +++ b/pkg/amqp-lib/composer.json @@ -8,15 +8,15 @@ "php": ">=5.6", "php-amqplib/php-amqplib": "^2.7@dev", "queue-interop/queue-interop": "^0.6@dev", - "queue-interop/amqp-interop": "^0.6@dev", - "enqueue/amqp-tools": "^0.7@dev" + "queue-interop/amqp-interop": "^0.7@dev", + "enqueue/amqp-tools": "^0.8@dev" }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/amqp-tools/ConnectionConfig.php b/pkg/amqp-tools/ConnectionConfig.php new file mode 100644 index 000000000..8a844d1b6 --- /dev/null +++ b/pkg/amqp-tools/ConnectionConfig.php @@ -0,0 +1,331 @@ +inputConfig = $config; + + $this->supportedSchemes = []; + $this->defaultConfig = [ + 'host' => 'localhost', + 'port' => 5672, + 'user' => 'guest', + 'pass' => 'guest', + 'vhost' => '/', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'heartbeat' => 0, + 'persisted' => false, + 'lazy' => true, + 'qos_global' => false, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + ]; + + $this->addSupportedScheme('amqp'); + } + + /** + * @param string $schema + * + * @return self + */ + public function addSupportedScheme($schema) + { + $this->supportedSchemes[] = $schema; + $this->supportedSchemes = array_unique($this->supportedSchemes); + + return $this; + } + + /** + * @param string $name + * @param mixed $value + * + * @return self + */ + public function addDefaultOption($name, $value) + { + $this->defaultConfig[$name] = $value; + + return $this; + } + + /** + * @return self + */ + public function parse() + { + if (empty($this->inputConfig) || in_array($this->inputConfig, $this->supportedSchemes, true)) { + $config = []; + } elseif (is_string($this->inputConfig)) { + $config = $this->parseDsn($this->inputConfig); + } elseif (is_array($this->inputConfig)) { + $config = $this->inputConfig; + if (array_key_exists('dsn', $config)) { + $dsn = $config['dsn']; + unset($config['dsn']); + + $config = array_replace($config, $this->parseDsn($dsn)); + } + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + + $config = array_replace($this->defaultConfig, $config); + $config['host'] = (string) $config['host']; + $config['port'] = (int) ($config['port']); + $config['user'] = (string) $config['user']; + $config['pass'] = (string) $config['pass']; + $config['read_timeout'] = max((float) ($config['read_timeout']), 0); + $config['write_timeout'] = max((float) ($config['write_timeout']), 0); + $config['connection_timeout'] = max((float) ($config['connection_timeout']), 0); + $config['heartbeat'] = max((float) ($config['heartbeat']), 0); + $config['persisted'] = !empty($config['persisted']); + $config['lazy'] = !empty($config['lazy']); + $config['qos_global'] = !empty($config['qos_global']); + $config['qos_prefetch_count'] = max((int) ($config['qos_prefetch_count']), 0); + $config['qos_prefetch_size'] = max((int) ($config['qos_prefetch_size']), 0); + + $this->config = $config; + + return $this; + } + + /** + * @return string + */ + public function getHost() + { + return $this->getOption('host'); + } + + /** + * @return int + */ + public function getPort() + { + return $this->getOption('port'); + } + + /** + * @return string + */ + public function getUser() + { + return $this->getOption('user'); + } + + /** + * @return string + */ + public function getPass() + { + return $this->getOption('pass'); + } + + /** + * @return string + */ + public function getVHost() + { + return $this->getOption('vhost'); + } + + /** + * @return int + */ + public function getReadTimeout() + { + return $this->getOption('read_timeout'); + } + + /** + * @return int + */ + public function getWriteTimeout() + { + return $this->getOption('write_timeout'); + } + + /** + * @return int + */ + public function getConnectionTimeout() + { + return $this->getOption('connection_timeout'); + } + + /** + * @return int + */ + public function getHeartbeat() + { + return $this->getOption('heartbeat'); + } + + /** + * @return bool + */ + public function isPersisted() + { + return $this->getOption('persisted'); + } + + /** + * @return bool + */ + public function isLazy() + { + return $this->getOption('lazy'); + } + + /** + * @return bool + */ + public function isQosGlobal() + { + return $this->getOption('qos_global'); + } + + /** + * @return int + */ + public function getQosPrefetchSize() + { + return $this->getOption('qos_prefetch_size'); + } + + /** + * @return int + */ + public function getQosPrefetchCount() + { + return $this->getOption('qos_prefetch_count'); + } + + /** + * @param string $name + * @param mixed $default + * + * @return bool + */ + public function getOption($name, $default = null) + { + $config = $this->getConfig(); + + return array_key_exists($name, $config) ? $config[$name] : $default; + } + + /** + * @throws \LogicException if the input config has not been parsed + * + * @return array + */ + public function getConfig() + { + if (null === $this->config) { + throw new \LogicException('The config has not been parsed.'); + } + + return $this->config; + } + + /** + * @param string $dsn + * + * @return array + */ + private function parseDsn($dsn) + { + if (false === parse_url($dsn)) { + throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); + } + + $config = []; + + $scheme = parse_url($dsn, PHP_URL_SCHEME); + if (false == in_array($scheme, $this->supportedSchemes, true)) { + throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be one of "%s" only.', $scheme, implode('", "', $this->supportedSchemes))); + } + + if ($host = parse_url($dsn, PHP_URL_HOST)) { + $config['host'] = $host; + } + if ($port = parse_url($dsn, PHP_URL_PORT)) { + $config['port'] = $port; + } + if ($user = parse_url($dsn, PHP_URL_USER)) { + $config['user'] = $user; + } + if ($pass = parse_url($dsn, PHP_URL_PASS)) { + $config['pass'] = $pass; + } + + if ($query = parse_url($dsn, PHP_URL_QUERY)) { + $queryConfig = []; + parse_str($query, $queryConfig); + + $config = array_replace($queryConfig, $config); + } + + if ($path = parse_url($dsn, PHP_URL_PATH)) { + $config['vhost'] = ltrim($path, '/'); + } + + return array_map('urldecode', $config); + } +} diff --git a/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php b/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php index dbcb71f5a..039d3db3b 100644 --- a/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php +++ b/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php @@ -15,16 +15,16 @@ public function registerDelayStrategy(ContainerBuilder $container, array $config if ($config['delay_strategy']) { $factory = $container->getDefinition($factoryId); - if (false == is_a($factory->getClass(), DelayStrategyAware::class, true)) { + if (false == (is_a($factory->getClass(), DelayStrategyAware::class, true) || $factory->getFactory())) { throw new \LogicException('Connection factory does not support delays'); } - if (strtolower($config['delay_strategy']) === 'dlx') { + if ('dlx' === strtolower($config['delay_strategy'])) { $delayId = sprintf('enqueue.client.%s.delay_strategy', $factoryName); $container->register($delayId, RabbitMqDlxDelayStrategy::class); $factory->addMethodCall('setDelayStrategy', [new Reference($delayId)]); - } elseif (strtolower($config['delay_strategy']) === 'delayed_message_plugin') { + } elseif ('delayed_message_plugin' === strtolower($config['delay_strategy'])) { $delayId = sprintf('enqueue.client.%s.delay_strategy', $factoryName); $container->register($delayId, RabbitMqDelayPluginDelayStrategy::class); diff --git a/pkg/amqp-tools/Tests/ConnectionConfigTest.php b/pkg/amqp-tools/Tests/ConnectionConfigTest.php new file mode 100644 index 000000000..8438ac19a --- /dev/null +++ b/pkg/amqp-tools/Tests/ConnectionConfigTest.php @@ -0,0 +1,343 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); + + (new ConnectionConfig(new \stdClass()))->parse(); + } + + public function testThrowIfSchemeIsNotSupported() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be one of "amqp" only.'); + + (new ConnectionConfig('http://example.com'))->parse(); + } + + public function testThrowIfSchemeIsNotSupportedIncludingAdditionalSupportedSchemes() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be one of "amqp", "amqp+foo" only.'); + + (new ConnectionConfig('http://example.com')) + ->addSupportedScheme('amqp+foo') + ->parse() + ; + } + + public function testThrowIfDsnCouldNotBeParsed() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Failed to parse DSN "amqp://:@/"'); + + (new ConnectionConfig('amqp://:@/'))->parse(); + } + + public function testShouldParseEmptyDsnWithDriverSet() + { + $config = (new ConnectionConfig('amqp+foo:')) + ->addSupportedScheme('amqp+foo') + ->parse() + ; + + $this->assertEquals([ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], $config->getConfig()); + } + + public function testShouldParseCustomDsnWithDriverSet() + { + $config = (new ConnectionConfig('amqp+foo://user:pass@host:10000/vhost')) + ->addSupportedScheme('amqp+foo') + ->parse() + ; + + $this->assertEquals([ + 'host' => 'host', + 'port' => 10000, + 'vhost' => 'vhost', + 'user' => 'user', + 'pass' => 'pass', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], $config->getConfig()); + } + + /** + * @dataProvider provideConfigs + * + * @param mixed $config + * @param mixed $expectedConfig + */ + public function testShouldParseConfigurationAsExpected($config, $expectedConfig) + { + $config = new ConnectionConfig($config); + $config->parse(); + + $this->assertEquals($expectedConfig, $config->getConfig()); + } + + public static function provideConfigs() + { + yield [ + null, + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + 'amqp:', + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + 'amqp://user:pass@host:10000/vhost', + [ + 'host' => 'host', + 'port' => 10000, + 'vhost' => 'vhost', + 'user' => 'user', + 'pass' => 'pass', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + 'amqp://user%61:%61pass@ho%61st:10000/v%2fhost', + [ + 'host' => 'hoast', + 'port' => 10000, + 'vhost' => 'v/host', + 'user' => 'usera', + 'pass' => 'apass', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + 'amqp://user:pass@host:10000/vhost?connection_timeout=20&write_timeout=4&read_timeout=-4&heartbeat=23.3', + [ + 'host' => 'host', + 'port' => 10000, + 'vhost' => 'vhost', + 'user' => 'user', + 'pass' => 'pass', + 'read_timeout' => 0., + 'write_timeout' => 4, + 'connection_timeout' => 20., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 23.3, + ], + ]; + + yield [ + 'amqp://user:pass@host:10000/vhost?persisted=1&lazy=&qos_global=true', + [ + 'host' => 'host', + 'port' => 10000, + 'vhost' => 'vhost', + 'user' => 'user', + 'pass' => 'pass', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => true, + 'lazy' => false, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => true, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + [], + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + ['lazy' => false, 'persisted' => 1, 'qos_global' => 1], + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => true, + 'lazy' => false, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => true, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + ['qos_prefetch_count' => 123, 'qos_prefetch_size' => -2], + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_count' => 123, + 'qos_prefetch_size' => 0, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + 'amqp://user:pass@host:10000/vhost?qos_prefetch_count=123&qos_prefetch_size=-2', + [ + 'host' => 'host', + 'port' => 10000, + 'vhost' => 'vhost', + 'user' => 'user', + 'pass' => 'pass', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_count' => 123, + 'qos_prefetch_size' => 0, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + + yield [ + [ + 'read_timeout' => 20., + 'write_timeout' => 30., + 'connection_timeout' => 40., + 'qos_prefetch_count' => 10, + 'dsn' => 'amqp://user:pass@host:10000/vhost?qos_prefetch_count=20', + ], + [ + 'host' => 'host', + 'port' => 10000, + 'vhost' => 'vhost', + 'user' => 'user', + 'pass' => 'pass', + 'read_timeout' => 20., + 'write_timeout' => 30., + 'connection_timeout' => 40., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_count' => 20, + 'qos_prefetch_size' => 0, + 'qos_global' => false, + 'heartbeat' => 0.0, + ], + ]; + } +} diff --git a/pkg/amqp-tools/composer.json b/pkg/amqp-tools/composer.json index 85dbf2753..f05356be9 100644 --- a/pkg/amqp-tools/composer.json +++ b/pkg/amqp-tools/composer.json @@ -7,12 +7,12 @@ "require": { "php": ">=5.6", "queue-interop/queue-interop": "^0.6@dev", - "queue-interop/amqp-interop": "^0.6@dev" + "queue-interop/amqp-interop": "^0.7@dev" }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/null": "^0.7@dev" + "enqueue/test": "^0.8@dev", + "enqueue/null": "^0.8@dev" }, "autoload": { "psr-4": { "Enqueue\\AmqpTools\\": "" }, @@ -23,7 +23,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/async-event-dispatcher/composer.json b/pkg/async-event-dispatcher/composer.json index f34253cff..35e39f3ac 100644 --- a/pkg/async-event-dispatcher/composer.json +++ b/pkg/async-event-dispatcher/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.6", - "enqueue/enqueue": "^0.7@dev", + "enqueue/enqueue": "^0.8@dev", "symfony/event-dispatcher": "^2.8|^3" }, "require-dev": { @@ -15,9 +15,9 @@ "symfony/config": "^2.8|^3", "symfony/http-kernel": "^2.8|^3", "symfony/filesystem": "^2.8|^3", - "enqueue/null": "^0.7@dev", - "enqueue/fs": "^0.7@dev", - "enqueue/test": "^0.7@dev" + "enqueue/null": "^0.8@dev", + "enqueue/fs": "^0.8@dev", + "enqueue/test": "^0.8@dev" }, "suggest": { "symfony/dependency-injection": "^2.8|^3 If you'd like to use async event dispatcher container extension." @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/dbal/Client/DbalDriver.php b/pkg/dbal/Client/DbalDriver.php index 8196a9b84..d5fd9a5a2 100644 --- a/pkg/dbal/Client/DbalDriver.php +++ b/pkg/dbal/Client/DbalDriver.php @@ -71,7 +71,7 @@ public function createTransportMessage(Message $message) $transportMessage->setProperties($properties); $transportMessage->setMessageId($message->getMessageId()); $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setDelay($message->getDelay()); + $transportMessage->setDeliveryDelay($message->getDelay()); $transportMessage->setReplyTo($message->getReplyTo()); $transportMessage->setCorrelationId($message->getCorrelationId()); if (array_key_exists($message->getPriority(), self::$priorityMap)) { @@ -97,7 +97,7 @@ public function createClientMessage(PsrMessage $message) $clientMessage->setContentType($message->getHeader('content_type')); $clientMessage->setMessageId($message->getMessageId()); $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setDelay($message->getDelay()); + $clientMessage->setDelay($message->getDeliveryDelay()); $clientMessage->setReplyTo($message->getReplyTo()); $clientMessage->setCorrelationId($message->getCorrelationId()); diff --git a/pkg/dbal/DbalConnectionFactory.php b/pkg/dbal/DbalConnectionFactory.php index f7d034849..55164213c 100644 --- a/pkg/dbal/DbalConnectionFactory.php +++ b/pkg/dbal/DbalConnectionFactory.php @@ -34,10 +34,10 @@ class DbalConnectionFactory implements PsrConnectionFactory * * @param array|string|null $config */ - public function __construct($config = 'mysql://') + public function __construct($config = 'mysql:') { if (empty($config)) { - $config = $this->parseDsn('mysql://'); + $config = $this->parseDsn('mysql:'); } elseif (is_string($config)) { $config = $this->parseDsn($config); } elseif (is_array($config)) { @@ -94,11 +94,14 @@ private function establishConnection() */ private function parseDsn($dsn) { - if (false === strpos($dsn, '://')) { - throw new \LogicException(sprintf('The given DSN "%s" is not valid. Must contain "://".', $dsn)); + if (false === parse_url($dsn)) { + throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); } - list($schema, $rest) = explode('://', $dsn, 2); + $schema = parse_url($dsn, PHP_URL_SCHEME); + if (empty($schema)) { + throw new \LogicException('Schema is empty'); + } $supported = [ 'db2' => true, @@ -128,7 +131,7 @@ private function parseDsn($dsn) return [ 'lazy' => true, 'connection' => [ - 'url' => empty($rest) ? $schema.'://root@localhost' : $dsn, + 'url' => $schema.':' === $dsn ? $schema.'://root@localhost' : $dsn, ], ]; } diff --git a/pkg/dbal/DbalConsumer.php b/pkg/dbal/DbalConsumer.php index 6368e24a2..f898e567f 100644 --- a/pkg/dbal/DbalConsumer.php +++ b/pkg/dbal/DbalConsumer.php @@ -169,31 +169,7 @@ protected function receiveMessage() try { $now = time(); - $query = $this->dbal->createQueryBuilder(); - $query - ->select('*') - ->from($this->context->getTableName()) - ->where('queue = :queue') - ->andWhere('(delayed_until IS NULL OR delayed_until <= :delayedUntil)') - ->orderBy('priority', 'desc') - ->addOrderBy('id', 'asc') - ->setMaxResults(1) - ; - - $sql = $query->getSQL().' '.$this->dbal->getDatabasePlatform()->getWriteLockSQL(); - - $dbalMessage = $this->dbal->executeQuery( - $sql, - [ - 'queue' => $this->queue->getQueueName(), - 'delayedUntil' => $now, - ], - [ - 'queue' => Type::STRING, - 'delayedUntil' => Type::INTEGER, - ] - )->fetch(); - + $dbalMessage = $this->fetchPrioritizedMessage($now) ?: $dbalMessage = $this->fetchMessage($now); if (false == $dbalMessage) { $this->dbal->commit(); @@ -211,9 +187,12 @@ protected function receiveMessage() $this->dbal->commit(); - return $this->convertMessage($dbalMessage); + if (empty($dbalMessage['time_to_live']) || $dbalMessage['time_to_live'] > time()) { + return $this->convertMessage($dbalMessage); + } } catch (\Exception $e) { $this->dbal->rollBack(); + throw $e; } } @@ -241,4 +220,70 @@ protected function convertMessage(array $dbalMessage) return $message; } + + /** + * @param int $now + * + * @return array|null + */ + private function fetchPrioritizedMessage($now) + { + $query = $this->dbal->createQueryBuilder(); + $query + ->select('*') + ->from($this->context->getTableName()) + ->andWhere('queue = :queue') + ->andWhere('priority IS NOT NULL') + ->andWhere('(delayed_until IS NULL OR delayed_until <= :delayedUntil)') + ->addOrderBy('priority', 'desc') + ->setMaxResults(1) + ; + + $sql = $query->getSQL().' '.$this->dbal->getDatabasePlatform()->getWriteLockSQL(); + + return $this->dbal->executeQuery( + $sql, + [ + 'queue' => $this->queue->getQueueName(), + 'delayedUntil' => $now, + ], + [ + 'queue' => Type::STRING, + 'delayedUntil' => Type::INTEGER, + ] + )->fetch(); + } + + /** + * @param int $now + * + * @return array|null + */ + private function fetchMessage($now) + { + $query = $this->dbal->createQueryBuilder(); + $query + ->select('*') + ->from($this->context->getTableName()) + ->andWhere('queue = :queue') + ->andWhere('priority IS NULL') + ->andWhere('(delayed_until IS NULL OR delayed_until <= :delayedUntil)') + ->addOrderBy('published_at', 'asc') + ->setMaxResults(1) + ; + + $sql = $query->getSQL().' '.$this->dbal->getDatabasePlatform()->getWriteLockSQL(); + + return $this->dbal->executeQuery( + $sql, + [ + 'queue' => $this->queue->getQueueName(), + 'delayedUntil' => $now, + ], + [ + 'queue' => Type::STRING, + 'delayedUntil' => Type::INTEGER, + ] + )->fetch(); + } } diff --git a/pkg/dbal/DbalContext.php b/pkg/dbal/DbalContext.php index 222492791..0570a4463 100644 --- a/pkg/dbal/DbalContext.php +++ b/pkg/dbal/DbalContext.php @@ -56,7 +56,7 @@ public function __construct($connection, array $config = []) * * @return DbalMessage */ - public function createMessage($body = null, array $properties = [], array $headers = []) + public function createMessage($body = '', array $properties = [], array $headers = []) { $message = new DbalMessage(); $message->setBody($body); @@ -170,8 +170,13 @@ public function createDataBaseTable() return; } + if ($this->getDbalConnection()->getDatabasePlatform()->hasNativeGuidType()) { + throw new \LogicException('The platform does not support UUIDs natively'); + } + $table = new Table($this->getTableName()); - $table->addColumn('id', 'integer', ['unsigned' => true, 'autoincrement' => true]); + $table->addColumn('id', 'guid'); + $table->addColumn('published_at', 'bigint'); $table->addColumn('body', 'text', ['notnull' => false]); $table->addColumn('headers', 'text', ['notnull' => false]); $table->addColumn('properties', 'text', ['notnull' => false]); @@ -179,8 +184,10 @@ public function createDataBaseTable() $table->addColumn('queue', 'string'); $table->addColumn('priority', 'smallint'); $table->addColumn('delayed_until', 'integer', ['notnull' => false]); + $table->addColumn('time_to_live', 'integer', ['notnull' => false]); $table->setPrimaryKey(['id']); + $table->addIndex(['published_at']); $table->addIndex(['queue']); $table->addIndex(['priority']); $table->addIndex(['delayed_until']); diff --git a/pkg/dbal/DbalMessage.php b/pkg/dbal/DbalMessage.php index a0409856e..979a53657 100644 --- a/pkg/dbal/DbalMessage.php +++ b/pkg/dbal/DbalMessage.php @@ -32,9 +32,14 @@ class DbalMessage implements PsrMessage private $priority; /** - * @var int + * @var int milliseconds + */ + private $deliveryDelay; + + /** + * @var int milliseconds */ - private $delay; + private $timeToLive; /** * @param string $body @@ -48,7 +53,7 @@ public function __construct($body = '', array $properties = [], array $headers = $this->headers = $headers; $this->redelivered = false; $this->priority = 0; - $this->delay = null; + $this->deliveryDelay = null; } /** @@ -182,19 +187,37 @@ public function setPriority($priority) /** * @return int */ - public function getDelay() + public function getDeliveryDelay() + { + return $this->deliveryDelay; + } + + /** + * Set delay in milliseconds. + * + * @param int $deliveryDelay + */ + public function setDeliveryDelay($deliveryDelay) + { + $this->deliveryDelay = $deliveryDelay; + } + + /** + * @return int|float|null + */ + public function getTimeToLive() { - return $this->delay; + return $this->timeToLive; } /** - * Set delay in seconds. + * Set time to live in milliseconds. * - * @param int $delay + * @param int|float|null $timeToLive */ - public function setDelay($delay) + public function setTimeToLive($timeToLive) { - $this->delay = $delay; + $this->timeToLive = $timeToLive; } /** diff --git a/pkg/dbal/DbalProducer.php b/pkg/dbal/DbalProducer.php index 85231d621..c8f58bc20 100644 --- a/pkg/dbal/DbalProducer.php +++ b/pkg/dbal/DbalProducer.php @@ -18,6 +18,16 @@ class DbalProducer implements PsrProducer */ private $priority; + /** + * @var int|float|null + */ + private $deliveryDelay; + + /** + * @var int|float|null + */ + private $timeToLive; + /** * @var DbalContext */ @@ -47,6 +57,12 @@ public function send(PsrDestination $destination, PsrMessage $message) if (null !== $this->priority && null === $message->getPriority()) { $message->setPriority($this->priority); } + if (null !== $this->deliveryDelay && null === $message->getDeliveryDelay()) { + $message->setDeliveryDelay($this->deliveryDelay); + } + if (null !== $this->timeToLive && null === $message->getTimeToLive()) { + $message->setTimeToLive($this->timeToLive); + } $body = $message->getBody(); if (is_scalar($body) || null === $body) { @@ -58,7 +74,16 @@ public function send(PsrDestination $destination, PsrMessage $message) )); } + $sql = 'SELECT '.$this->context->getDbalConnection()->getDatabasePlatform()->getGuidExpression(); + $uuid = $this->context->getDbalConnection()->query($sql)->fetchColumn(0); + + if (empty($uuid)) { + throw new \LogicException('The generated uuid is empty'); + } + $dbalMessage = [ + 'id' => $uuid, + 'published_at' => (int) microtime(true) * 10000, 'body' => $body, 'headers' => JSON::encode($message->getHeaders()), 'properties' => JSON::encode($message->getProperties()), @@ -66,7 +91,7 @@ public function send(PsrDestination $destination, PsrMessage $message) 'queue' => $destination->getQueueName(), ]; - $delay = $message->getDelay(); + $delay = $message->getDeliveryDelay(); if ($delay) { if (!is_int($delay)) { throw new \LogicException(sprintf( @@ -79,16 +104,35 @@ public function send(PsrDestination $destination, PsrMessage $message) throw new \LogicException(sprintf('Delay must be positive integer but got: "%s"', $delay)); } - $dbalMessage['delayed_until'] = time() + $delay; + $dbalMessage['delayed_until'] = time() + (int) $delay / 1000; + } + + $timeToLive = $message->getTimeToLive(); + if ($timeToLive) { + if (!is_int($timeToLive)) { + throw new \LogicException(sprintf( + 'TimeToLive must be integer but got: "%s"', + is_object($timeToLive) ? get_class($timeToLive) : gettype($timeToLive) + )); + } + + if ($timeToLive <= 0) { + throw new \LogicException(sprintf('TimeToLive must be positive integer but got: "%s"', $timeToLive)); + } + + $dbalMessage['time_to_live'] = time() + (int) $timeToLive / 1000; } try { $this->context->getDbalConnection()->insert($this->context->getTableName(), $dbalMessage, [ + 'id' => Type::GUID, + 'published_at' => Type::INTEGER, 'body' => Type::TEXT, 'headers' => Type::TEXT, 'properties' => Type::TEXT, 'priority' => Type::SMALLINT, 'queue' => Type::STRING, + 'time_to_live' => Type::INTEGER, 'delayed_until' => Type::INTEGER, ]); } catch (\Exception $e) { @@ -101,11 +145,9 @@ public function send(PsrDestination $destination, PsrMessage $message) */ public function setDeliveryDelay($deliveryDelay) { - if (null === $deliveryDelay) { - return; - } + $this->deliveryDelay = $deliveryDelay; - throw new \LogicException('Not implemented'); + return $this; } /** @@ -113,7 +155,7 @@ public function setDeliveryDelay($deliveryDelay) */ public function getDeliveryDelay() { - return null; + return $this->deliveryDelay; } /** @@ -139,11 +181,7 @@ public function getPriority() */ public function setTimeToLive($timeToLive) { - if (null === $timeToLive) { - return; - } - - throw new \LogicException('Not implemented'); + $this->timeToLive = $timeToLive; } /** @@ -151,6 +189,6 @@ public function setTimeToLive($timeToLive) */ public function getTimeToLive() { - return null; + return $this->timeToLive; } } diff --git a/pkg/dbal/Tests/Client/DbalDriverTest.php b/pkg/dbal/Tests/Client/DbalDriverTest.php index 232e3ceaf..6c8de2016 100644 --- a/pkg/dbal/Tests/Client/DbalDriverTest.php +++ b/pkg/dbal/Tests/Client/DbalDriverTest.php @@ -93,7 +93,7 @@ public function testShouldConvertTransportMessageToClientMessage() $transportMessage->setMessageId('MessageId'); $transportMessage->setTimestamp(1000); $transportMessage->setPriority(2); - $transportMessage->setDelay(12345); + $transportMessage->setDeliveryDelay(12345); $driver = new DbalDriver( $this->createPsrContextMock(), diff --git a/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php b/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php index 39a29563d..eeaf31c34 100644 --- a/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php +++ b/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php @@ -32,7 +32,7 @@ public function testThrowIfSchemeIsNotSupported() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN "invalidDSN" is not valid. Must contain "://".'); + $this->expectExceptionMessage('Schema is empty'); new DbalConnectionFactory('invalidDSN'); } @@ -63,7 +63,7 @@ public static function provideConfigs() ]; yield [ - 'mysql://', + 'mysql:', [ 'lazy' => true, 'connection' => [ @@ -73,7 +73,7 @@ public static function provideConfigs() ]; yield [ - 'pgsql://', + 'pgsql:', [ 'lazy' => true, 'connection' => [ diff --git a/pkg/dbal/Tests/DbalConsumerTest.php b/pkg/dbal/Tests/DbalConsumerTest.php index 6fcf7fa9e..87c4233d3 100644 --- a/pkg/dbal/Tests/DbalConsumerTest.php +++ b/pkg/dbal/Tests/DbalConsumerTest.php @@ -131,304 +131,6 @@ public function testRejectShouldThrowIfMessageWasNotInserted() $consumer->reject($message, true); } - public function testShouldReceiveMessage() - { - $dbalMessage = [ - 'id' => 'id', - 'body' => 'body', - 'headers' => '{"hkey":"hvalue"}', - 'properties' => '{"pkey":"pvalue"}', - 'priority' => 5, - 'queue' => 'queue', - 'redelivered' => true, - ]; - - $statement = $this->createStatementMock(); - $statement - ->expects($this->once()) - ->method('fetch') - ->will($this->returnValue($dbalMessage)) - ; - - $queryBuilder = $this->createQueryBuilderMock(); - $queryBuilder - ->expects($this->once()) - ->method('select') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('from') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('where') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('andWhere') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->exactly(1)) - ->method('orderBy') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->exactly(1)) - ->method('addOrderBy') - ->will($this->returnSelf()) - ; - - $platform = $this->createPlatformMock(); - - $dbal = $this->createConnectionMock(); - $dbal - ->expects($this->once()) - ->method('createQueryBuilder') - ->willReturn($queryBuilder) - ; - $dbal - ->expects($this->once()) - ->method('executeQuery') - ->willReturn($statement) - ; - $dbal - ->expects($this->once()) - ->method('delete') - ->willReturn(1) - ; - $dbal - ->expects($this->once()) - ->method('commit') - ; - $dbal - ->expects($this->once()) - ->method('getDatabasePlatform') - ->willReturn($platform) - ; - - $context = $this->createContextMock(); - $context - ->expects($this->once()) - ->method('getDbalConnection') - ->will($this->returnValue($dbal)) - ; - $context - ->expects($this->atLeastOnce()) - ->method('getTableName') - ->will($this->returnValue('tableName')) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new DbalMessage()) - ; - - $consumer = new DbalConsumer($context, new DbalDestination('queue')); - $result = $consumer->receiveNoWait(); - - $this->assertInstanceOf(DbalMessage::class, $result); - $this->assertEquals('body', $result->getBody()); - $this->assertEquals(['hkey' => 'hvalue'], $result->getHeaders()); - $this->assertEquals(['pkey' => 'pvalue'], $result->getProperties()); - $this->assertTrue($result->isRedelivered()); - $this->assertEquals(5, $result->getPriority()); - } - - public function testShouldReturnNullIfThereIsNoNewMessage() - { - $statement = $this->createStatementMock(); - $statement - ->expects($this->once()) - ->method('fetch') - ->will($this->returnValue(null)) - ; - - $queryBuilder = $this->createQueryBuilderMock(); - $queryBuilder - ->expects($this->once()) - ->method('select') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('from') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('where') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('andWhere') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->exactly(1)) - ->method('orderBy') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->exactly(1)) - ->method('addOrderBy') - ->will($this->returnSelf()) - ; - - $platform = $this->createPlatformMock(); - - $dbal = $this->createConnectionMock(); - $dbal - ->expects($this->once()) - ->method('createQueryBuilder') - ->willReturn($queryBuilder) - ; - $dbal - ->expects($this->once()) - ->method('executeQuery') - ->willReturn($statement) - ; - $dbal - ->expects($this->never()) - ->method('delete') - ->willReturn(1) - ; - $dbal - ->expects($this->once()) - ->method('commit') - ; - $dbal - ->expects($this->once()) - ->method('getDatabasePlatform') - ->willReturn($platform) - ; - - $context = $this->createContextMock(); - $context - ->expects($this->once()) - ->method('getDbalConnection') - ->will($this->returnValue($dbal)) - ; - $context - ->expects($this->atLeastOnce()) - ->method('getTableName') - ->will($this->returnValue('tableName')) - ; - $context - ->expects($this->never()) - ->method('createMessage') - ->willReturn(new DbalMessage()) - ; - - $consumer = new DbalConsumer($context, new DbalDestination('queue')); - $consumer->setPollingInterval(1000); - $result = $consumer->receive(.000001); - - $this->assertEmpty($result); - } - - public function testShouldThrowIfMessageWasNotRemoved() - { - $statement = $this->createStatementMock(); - $statement - ->expects($this->once()) - ->method('fetch') - ->will($this->returnValue(['id' => '2134'])) - ; - - $queryBuilder = $this->createQueryBuilderMock(); - $queryBuilder - ->expects($this->once()) - ->method('select') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('from') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('where') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->once()) - ->method('andWhere') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->exactly(1)) - ->method('orderBy') - ->will($this->returnSelf()) - ; - $queryBuilder - ->expects($this->exactly(1)) - ->method('addOrderBy') - ->will($this->returnSelf()) - ; - - $platform = $this->createPlatformMock(); - - $dbal = $this->createConnectionMock(); - $dbal - ->expects($this->once()) - ->method('createQueryBuilder') - ->willReturn($queryBuilder) - ; - $dbal - ->expects($this->once()) - ->method('executeQuery') - ->willReturn($statement) - ; - $dbal - ->expects($this->once()) - ->method('delete') - ->willReturn(0) - ; - $dbal - ->expects($this->never()) - ->method('commit') - ; - $dbal - ->expects($this->once()) - ->method('rollBack') - ; - $dbal - ->expects($this->once()) - ->method('getDatabasePlatform') - ->willReturn($platform) - ; - - $context = $this->createContextMock(); - $context - ->expects($this->once()) - ->method('getDbalConnection') - ->will($this->returnValue($dbal)) - ; - $context - ->expects($this->atLeastOnce()) - ->method('getTableName') - ->will($this->returnValue('tableName')) - ; - $context - ->expects($this->never()) - ->method('createMessage') - ->willReturn(new DbalMessage()) - ; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Expected record was removed but it is not. id: "2134"'); - - $consumer = new DbalConsumer($context, new DbalDestination('queue')); - $consumer->setPollingInterval(1000); - $consumer->receive(.000001); - } - /** * @return \PHPUnit_Framework_MockObject_MockObject|Connection */ diff --git a/pkg/dbal/Tests/DbalMessageTest.php b/pkg/dbal/Tests/DbalMessageTest.php index a31c37e93..c0af060d5 100644 --- a/pkg/dbal/Tests/DbalMessageTest.php +++ b/pkg/dbal/Tests/DbalMessageTest.php @@ -38,7 +38,7 @@ public function testShouldSetDelayToNullInConstructor() { $message = new DbalMessage(); - $this->assertNull($message->getDelay()); + $this->assertNull($message->getDeliveryDelay()); } public function testShouldSetCorrelationIdAsHeader() diff --git a/pkg/dbal/Tests/DbalProducerTest.php b/pkg/dbal/Tests/DbalProducerTest.php index b9ad69471..370c94a22 100644 --- a/pkg/dbal/Tests/DbalProducerTest.php +++ b/pkg/dbal/Tests/DbalProducerTest.php @@ -8,7 +8,6 @@ use Enqueue\Dbal\DbalMessage; use Enqueue\Dbal\DbalProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\Exception; use Interop\Queue\InvalidDestinationException; use Interop\Queue\InvalidMessageException; use Interop\Queue\PsrDestination; @@ -54,72 +53,6 @@ public function testShouldThrowIfDestinationOfInvalidType() $producer->send(new NotSupportedDestination1(), new DbalMessage()); } - public function testShouldThrowIfInsertMessageFailed() - { - $dbal = $this->createConnectionMock(); - $dbal - ->expects($this->once()) - ->method('insert') - ->will($this->throwException(new \Exception('error message'))) - ; - - $context = $this->createContextMock(); - $context - ->expects($this->once()) - ->method('getDbalConnection') - ->will($this->returnValue($dbal)) - ; - - $destination = new DbalDestination('queue-name'); - $message = new DbalMessage(); - - $this->expectException(Exception::class); - $this->expectExceptionMessage('The transport fails to send the message due to some internal error.'); - - $producer = new DbalProducer($context); - $producer->send($destination, $message); - } - - public function testShouldSendMessage() - { - $expectedMessage = [ - 'body' => 'body', - 'headers' => '{"hkey":"hvalue"}', - 'properties' => '{"pkey":"pvalue"}', - 'priority' => 123, - 'queue' => 'queue-name', - ]; - - $dbal = $this->createConnectionMock(); - $dbal - ->expects($this->once()) - ->method('insert') - ->with('tableName', $expectedMessage) - ; - - $context = $this->createContextMock(); - $context - ->expects($this->once()) - ->method('getDbalConnection') - ->will($this->returnValue($dbal)) - ; - $context - ->expects($this->once()) - ->method('getTableName') - ->will($this->returnValue('tableName')) - ; - - $destination = new DbalDestination('queue-name'); - $message = new DbalMessage(); - $message->setBody('body'); - $message->setHeaders(['hkey' => 'hvalue']); - $message->setProperties(['pkey' => 'pvalue']); - $message->setPriority(123); - - $producer = new DbalProducer($context); - $producer->send($destination, $message); - } - /** * @return \PHPUnit_Framework_MockObject_MockObject|DbalContext */ diff --git a/pkg/dbal/Tests/Spec/CreateDbalContextTrait.php b/pkg/dbal/Tests/Spec/CreateDbalContextTrait.php new file mode 100644 index 000000000..d4c954afb --- /dev/null +++ b/pkg/dbal/Tests/Spec/CreateDbalContextTrait.php @@ -0,0 +1,27 @@ +markTestSkipped('The DOCTRINE_DSN env is not available. Skip tests'); + } + + $factory = new DbalConnectionFactory($env); + + $context = $factory->createContext(); + + if ($context->getDbalConnection()->getSchemaManager()->tablesExist([$context->getTableName()])) { + $context->getDbalConnection()->getSchemaManager()->dropTable($context->getTableName()); + } + + $context->createDataBaseTable(); + + return $context; + } +} diff --git a/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php b/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php new file mode 100644 index 000000000..2ed159787 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php @@ -0,0 +1,17 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalProducerTest.php b/pkg/dbal/Tests/Spec/DbalProducerTest.php new file mode 100644 index 000000000..2580d3fd3 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalProducerTest.php @@ -0,0 +1,21 @@ +createDbalContext()->createProducer(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalQueueTest.php b/pkg/dbal/Tests/Spec/DbalQueueTest.php new file mode 100644 index 000000000..091a48046 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalQueueTest.php @@ -0,0 +1,17 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php index 8ebf430f0..5ddc64509 100644 --- a/pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php +++ b/pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php @@ -2,8 +2,6 @@ namespace Enqueue\Dbal\Tests\Spec; -use Enqueue\Dbal\DbalConnectionFactory; -use Enqueue\Dbal\DbalDestination; use Enqueue\Dbal\DbalMessage; use Interop\Queue\PsrContext; use Interop\Queue\Spec\SendAndReceivePriorityMessagesFromQueueSpec; @@ -13,32 +11,14 @@ */ class DbalSendAndReceivePriorityMessagesFromQueueTest extends SendAndReceivePriorityMessagesFromQueueSpec { - public function test() - { - $this->markTestSkipped('Skip for now. The dbal transport will be reworked in 0.8'); - } + use CreateDbalContextTrait; /** * @return PsrContext */ protected function createContext() { - $factory = new DbalConnectionFactory([ - 'lazy' => true, - 'connection' => [ - 'dbname' => getenv('SYMFONY__DB__NAME'), - 'user' => getenv('SYMFONY__DB__USER'), - 'password' => getenv('SYMFONY__DB__PASSWORD'), - 'host' => getenv('SYMFONY__DB__HOST'), - 'port' => getenv('SYMFONY__DB__PORT'), - 'driver' => getenv('SYMFONY__DB__DRIVER'), - ], - ]); - - $context = $factory->createContext(); - $context->createDataBaseTable(); - - return $context; + return $this->createDbalContext(); } /** @@ -54,14 +34,4 @@ protected function createMessage(PsrContext $context, $priority) return $message; } - - /** - * {@inheritdoc} - * - * @return DbalDestination - */ - protected function createQueue(PsrContext $context, $queueName) - { - return parent::createQueue($context, $queueName.time()); - } } diff --git a/pkg/dbal/Tests/Spec/DbalSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/dbal/Tests/Spec/DbalSendAndReceiveTimeToLiveMessagesFromQueueTest.php new file mode 100644 index 000000000..478005030 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -0,0 +1,21 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromQueueTest.php b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromQueueTest.php new file mode 100644 index 000000000..84ae52345 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromQueueTest.php @@ -0,0 +1,21 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromTopicTest.php b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromTopicTest.php new file mode 100644 index 000000000..c2b6c085b --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromTopicTest.php @@ -0,0 +1,21 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromQueueTest.php b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromQueueTest.php new file mode 100644 index 000000000..523673d1c --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromQueueTest.php @@ -0,0 +1,21 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromTopicTest.php b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromTopicTest.php new file mode 100644 index 000000000..e8f94bb44 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromTopicTest.php @@ -0,0 +1,21 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/DbalTopicTest.php b/pkg/dbal/Tests/Spec/DbalTopicTest.php new file mode 100644 index 000000000..91bd52fd0 --- /dev/null +++ b/pkg/dbal/Tests/Spec/DbalTopicTest.php @@ -0,0 +1,17 @@ +getExtension('enqueue'); - if (class_exists(StompContext::class)) { + if (class_exists(StompConnectionFactory::class)) { $extension->addTransportFactory(new StompTransportFactory()); $extension->addTransportFactory(new RabbitMqStompTransportFactory()); } - if (class_exists(AmqpContext::class)) { - $extension->addTransportFactory(new AmqpTransportFactory()); - $extension->addTransportFactory(new RabbitMqAmqpTransportFactory()); - } - - if (class_exists(AmqpLibContext::class)) { - $extension->addTransportFactory(new AmqpLibTransportFactory()); - $extension->addTransportFactory(new RabbitMqAmqpLibTransportFactory()); - } + $extension->addTransportFactory(new AmqpTransportFactory('amqp')); + $extension->addTransportFactory(new RabbitMqAmqpTransportFactory('rabbitmq_amqp')); - if (class_exists(FsContext::class)) { + if (class_exists(FsConnectionFactory::class)) { $extension->addTransportFactory(new FsTransportFactory()); } - if (class_exists(RedisContext::class)) { + if (class_exists(RedisConnectionFactory::class)) { $extension->addTransportFactory(new RedisTransportFactory()); } - if (class_exists(DbalContext::class)) { + if (class_exists(DbalConnectionFactory::class)) { $extension->addTransportFactory(new DbalTransportFactory()); } - if (class_exists(SqsContext::class)) { + if (class_exists(SqsConnectionFactory::class)) { $extension->addTransportFactory(new SqsTransportFactory()); } - if (class_exists(AmqpBunnyContext::class)) { - $extension->addTransportFactory(new AmqpBunnyTransportFactory()); - $extension->addTransportFactory(new RabbitMqAmqpBunnyTransportFactory()); - } - $container->addCompilerPass(new AsyncEventsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); $container->addCompilerPass(new AsyncTransformersPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); } diff --git a/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php b/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php index da103744c..1992dd8d5 100644 --- a/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php @@ -27,6 +27,7 @@ public function provideEnqueueConfigs() 'transport' => [ 'default' => 'amqp', 'amqp' => [ + 'driver' => 'ext', 'host' => getenv('SYMFONY__RABBITMQ__HOST'), 'port' => getenv('SYMFONY__RABBITMQ__AMQP__PORT'), 'user' => getenv('SYMFONY__RABBITMQ__USER'), diff --git a/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php b/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php index 3b8d69a9d..638f6f4c2 100644 --- a/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php @@ -2,12 +2,6 @@ namespace Enqueue\Bundle\Tests\Unit; -use Enqueue\AmqpBunny\Symfony\AmqpBunnyTransportFactory; -use Enqueue\AmqpBunny\Symfony\RabbitMqAmqpBunnyTransportFactory; -use Enqueue\AmqpExt\Symfony\AmqpTransportFactory; -use Enqueue\AmqpExt\Symfony\RabbitMqAmqpTransportFactory; -use Enqueue\AmqpLib\Symfony\AmqpLibTransportFactory; -use Enqueue\AmqpLib\Symfony\RabbitMqAmqpLibTransportFactory; use Enqueue\Bundle\DependencyInjection\Compiler\BuildClientExtensionsPass; use Enqueue\Bundle\DependencyInjection\Compiler\BuildClientRoutingPass; use Enqueue\Bundle\DependencyInjection\Compiler\BuildConsumptionExtensionsPass; @@ -23,6 +17,8 @@ use Enqueue\Sqs\Symfony\SqsTransportFactory; use Enqueue\Stomp\Symfony\RabbitMqStompTransportFactory; use Enqueue\Stomp\Symfony\StompTransportFactory; +use Enqueue\Symfony\AmqpTransportFactory; +use Enqueue\Symfony\RabbitMqAmqpTransportFactory; use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -125,33 +121,17 @@ public function testShouldRegisterAmqpAndRabbitMqAmqpTransportFactories() ->expects($this->at(2)) ->method('addTransportFactory') ->with($this->isInstanceOf(AmqpTransportFactory::class)) + ->willReturnCallback(function (AmqpTransportFactory $factory) { + $this->assertSame('amqp', $factory->getName()); + }) ; $extensionMock ->expects($this->at(3)) ->method('addTransportFactory') ->with($this->isInstanceOf(RabbitMqAmqpTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterAmqpLibAndRabbitMqAmqpLibTransportFactories() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(4)) - ->method('addTransportFactory') - ->with($this->isInstanceOf(AmqpLibTransportFactory::class)) - ; - $extensionMock - ->expects($this->at(5)) - ->method('addTransportFactory') - ->with($this->isInstanceOf(RabbitMqAmqpLibTransportFactory::class)) + ->willReturnCallback(function (RabbitMqAmqpTransportFactory $factory) { + $this->assertSame('rabbitmq_amqp', $factory->getName()); + }) ; $bundle = new EnqueueBundle(); @@ -166,7 +146,7 @@ public function testShouldRegisterFSTransportFactory() $container->registerExtension($extensionMock); $extensionMock - ->expects($this->at(6)) + ->expects($this->at(4)) ->method('addTransportFactory') ->with($this->isInstanceOf(FsTransportFactory::class)) ; @@ -183,7 +163,7 @@ public function testShouldRegisterRedisTransportFactory() $container->registerExtension($extensionMock); $extensionMock - ->expects($this->at(7)) + ->expects($this->at(5)) ->method('addTransportFactory') ->with($this->isInstanceOf(RedisTransportFactory::class)) ; @@ -200,7 +180,7 @@ public function testShouldRegisterDbalTransportFactory() $container->registerExtension($extensionMock); $extensionMock - ->expects($this->at(8)) + ->expects($this->at(6)) ->method('addTransportFactory') ->with($this->isInstanceOf(DbalTransportFactory::class)) ; @@ -217,7 +197,7 @@ public function testShouldRegisterSqsTransportFactory() $container->registerExtension($extensionMock); $extensionMock - ->expects($this->at(9)) + ->expects($this->at(7)) ->method('addTransportFactory') ->with($this->isInstanceOf(SqsTransportFactory::class)) ; @@ -226,28 +206,6 @@ public function testShouldRegisterSqsTransportFactory() $bundle->build($container); } - public function testShouldRegisterAmqpBunnyTransportFactory() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(10)) - ->method('addTransportFactory') - ->with($this->isInstanceOf(AmqpBunnyTransportFactory::class)) - ; - $extensionMock - ->expects($this->at(11)) - ->method('addTransportFactory') - ->with($this->isInstanceOf(RabbitMqAmqpBunnyTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - /** * @return \PHPUnit_Framework_MockObject_MockObject|EnqueueExtension */ diff --git a/pkg/enqueue-bundle/composer.json b/pkg/enqueue-bundle/composer.json index 742fea0af..4592a6dbc 100644 --- a/pkg/enqueue-bundle/composer.json +++ b/pkg/enqueue-bundle/composer.json @@ -7,23 +7,23 @@ "require": { "php": ">=5.6", "symfony/framework-bundle": "^2.8|^3", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "enqueue/async-event-dispatcher": "^0.7@dev" + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "enqueue/async-event-dispatcher": "^0.8@dev" }, "require-dev": { "phpunit/phpunit": "~5.5", - "enqueue/stomp": "^0.7@dev", - "enqueue/amqp-ext": "^0.7@dev", + "enqueue/stomp": "^0.8@dev", + "enqueue/amqp-ext": "^0.8@dev", "php-amqplib/php-amqplib": "^2.7@dev", - "enqueue/amqp-lib": "^0.7@dev", - "enqueue/amqp-bunny": "^0.7@dev", - "enqueue/job-queue": "^0.7@dev", - "enqueue/fs": "^0.7@dev", - "enqueue/redis": "^0.7@dev", - "enqueue/dbal": "^0.7@dev", - "enqueue/sqs": "^0.7@dev", - "enqueue/test": "^0.7@dev", + "enqueue/amqp-lib": "^0.8@dev", + "enqueue/amqp-bunny": "^0.8@dev", + "enqueue/job-queue": "^0.8@dev", + "enqueue/fs": "^0.8@dev", + "enqueue/redis": "^0.8@dev", + "enqueue/dbal": "^0.8@dev", + "enqueue/sqs": "^0.8@dev", + "enqueue/test": "^0.8@dev", "doctrine/doctrine-bundle": "~1.2", "symfony/monolog-bundle": "^2.8|^3", "symfony/browser-kit": "^2.8|^3", @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/enqueue/Consumption/QueueConsumer.php b/pkg/enqueue/Consumption/QueueConsumer.php index fe2aafcee..4e59abcd8 100644 --- a/pkg/enqueue/Consumption/QueueConsumer.php +++ b/pkg/enqueue/Consumption/QueueConsumer.php @@ -6,6 +6,9 @@ use Enqueue\Consumption\Exception\InvalidArgumentException; use Enqueue\Consumption\Exception\LogicException; use Enqueue\Util\VarExport; +use Interop\Amqp\AmqpConsumer; +use Interop\Amqp\AmqpContext; +use Interop\Amqp\AmqpMessage; use Interop\Queue\PsrConsumer; use Interop\Queue\PsrContext; use Interop\Queue\PsrProcessor; @@ -143,6 +146,10 @@ public function bind($queue, $processor) */ public function consume(ExtensionInterface $runtimeExtension = null) { + if (empty($this->boundProcessors)) { + throw new \LogicException('There is nothing to consume. It is required to bind something before calling consume method.'); + } + /** @var PsrConsumer[] $consumers */ $consumers = []; /** @var PsrQueue $queue */ @@ -163,21 +170,65 @@ public function consume(ExtensionInterface $runtimeExtension = null) while (true) { try { - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - $consumer = $consumers[$queue->getQueueName()]; - - $context = new Context($this->psrContext); - $context->setLogger($logger); - $context->setPsrQueue($queue); - $context->setPsrConsumer($consumer); - $context->setPsrProcessor($processor); - - $this->doConsume($extension, $context); + if ($this->psrContext instanceof AmqpContext) { + $callback = function (AmqpMessage $message, AmqpConsumer $consumer) use ($extension, $logger) { + $currentProcessor = null; + + /** @var PsrQueue $queue */ + foreach ($this->boundProcessors as list($queue, $processor)) { + if ($queue->getQueueName() === $consumer->getQueue()->getQueueName()) { + $currentProcessor = $processor; + } + } + + if (false == $currentProcessor) { + throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $consumer->getQueue()->getQueueName())); + } + + $context = new Context($this->psrContext); + $context->setLogger($logger); + $context->setPsrQueue($consumer->getQueue()); + $context->setPsrConsumer($consumer); + $context->setPsrProcessor($currentProcessor); + $context->setPsrMessage($message); + + $this->doConsume($extension, $context); + + return true; + }; + + foreach ($consumers as $consumer) { + /* @var AmqpConsumer $consumer */ + + $this->psrContext->subscribe($consumer, $callback); + } + + $this->psrContext->consume($this->receiveTimeout); + } else { + /** @var PsrQueue $queue */ + foreach ($this->boundProcessors as list($queue, $processor)) { + $consumer = $consumers[$queue->getQueueName()]; + + $context = new Context($this->psrContext); + $context->setLogger($logger); + $context->setPsrQueue($queue); + $context->setPsrConsumer($consumer); + $context->setPsrProcessor($processor); + + $this->doConsume($extension, $context); + } } } catch (ConsumptionInterruptedException $e) { $logger->info(sprintf('Consuming interrupted')); + if ($this->psrContext instanceof AmqpContext) { + foreach ($consumers as $consumer) { + /* @var AmqpConsumer $consumer */ + + $this->psrContext->unsubscribe($consumer); + } + } + $context->setExecutionInterrupted(true); $extension->onInterrupted($context); @@ -212,20 +263,27 @@ protected function doConsume(ExtensionInterface $extension, Context $context) $consumer = $context->getPsrConsumer(); $logger = $context->getLogger(); - $extension->onBeforeReceive($context); + if (false == $context->getPsrMessage() instanceof AmqpContext) { + $extension->onBeforeReceive($context); + } if ($context->isExecutionInterrupted()) { throw new ConsumptionInterruptedException(); } - if ($message = $consumer->receive($this->receiveTimeout)) { + $message = $context->getPsrMessage(); + if (false == $message) { + if ($message = $consumer->receive($this->receiveTimeout)) { + $context->setPsrMessage($message); + } + } + + if ($message) { $logger->info('Message received from the queue: '.$context->getPsrQueue()->getQueueName()); $logger->debug('Headers: {headers}', ['headers' => new VarExport($message->getHeaders())]); $logger->debug('Properties: {properties}', ['properties' => new VarExport($message->getProperties())]); $logger->debug('Payload: {payload}', ['payload' => new VarExport($message->getBody())]); - $context->setPsrMessage($message); - $extension->onPreReceived($context); if (!$context->getResult()) { $result = $processor->process($message, $this->psrContext); @@ -251,6 +309,10 @@ protected function doConsume(ExtensionInterface $extension, Context $context) $logger->info(sprintf('Message processed: %s', $context->getResult())); $extension->onPostReceived($context); + + if ($context->getPsrMessage() instanceof AmqpContext) { + $extension->onBeforeReceive($context); + } } else { usleep($this->idleTimeout * 1000); $extension->onIdle($context); diff --git a/pkg/amqp-ext/Symfony/AmqpTransportFactory.php b/pkg/enqueue/Symfony/AmqpTransportFactory.php similarity index 51% rename from pkg/amqp-ext/Symfony/AmqpTransportFactory.php rename to pkg/enqueue/Symfony/AmqpTransportFactory.php index e2a80b826..8b64a61f2 100644 --- a/pkg/amqp-ext/Symfony/AmqpTransportFactory.php +++ b/pkg/enqueue/Symfony/AmqpTransportFactory.php @@ -1,16 +1,18 @@ beforeNormalization() - ->ifString() + ->ifTrue(function ($v) { + return empty($v); + }) + ->then(function ($v) { + return ['dsn' => 'amqp:']; + }) + ->ifString() ->then(function ($v) { return ['dsn' => $v]; }) ->end() ->children() + ->enumNode('driver') + ->values($drivers) + ->end() ->scalarNode('dsn') - ->info('The connection to AMQP broker set as a string. Other parameters are ignored if set') + ->info('The connection to AMQP broker set as a string. Other parameters could be used as defaults') ->end() ->scalarNode('host') - ->defaultValue('localhost') - ->cannotBeEmpty() ->info('The host to connect too. Note: Max 1024 characters') ->end() ->scalarNode('port') - ->defaultValue(5672) - ->cannotBeEmpty() ->info('Port on the host.') ->end() ->scalarNode('user') - ->defaultValue('guest') - ->cannotBeEmpty() ->info('The user name to use. Note: Max 128 characters.') ->end() ->scalarNode('pass') - ->defaultValue('guest') - ->cannotBeEmpty() ->info('Password. Note: Max 128 characters.') ->end() ->scalarNode('vhost') - ->defaultValue('/') - ->cannotBeEmpty() ->info('The virtual host on the host. Note: Max 128 characters.') ->end() - ->integerNode('connect_timeout') + ->floatNode('connection_timeout') ->min(0) ->info('Connection timeout. Note: 0 or greater seconds. May be fractional.') ->end() - ->integerNode('read_timeout') + ->floatNode('read_timeout') ->min(0) ->info('Timeout in for income activity. Note: 0 or greater seconds. May be fractional.') ->end() - ->integerNode('write_timeout') + ->floatNode('write_timeout') ->min(0) ->info('Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional.') ->end() - ->booleanNode('persisted') - ->defaultFalse() - ->end() - ->booleanNode('lazy') - ->defaultTrue() + ->floatNode('heartbeat') + ->min(0) + ->info('How often to send heartbeat. 0 means off.') ->end() + ->booleanNode('persisted')->end() + ->booleanNode('lazy')->end() ->enumNode('receive_method') ->values(['basic_get', 'basic_consume']) - ->defaultValue('basic_get') ->info('The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher') ->end() + ->floatNode('qos_prefetch_size') + ->min(0) + ->info('The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit"') + ->end() + ->floatNode('qos_prefetch_count') + ->min(0) + ->info('Specifies a prefetch window in terms of whole messages') + ->end() + ->booleanNode('qos_global') + ->info('If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection.') + ->end() + ->variableNode('driver_options') + ->info('The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option.') + ->end() ; } @@ -99,8 +124,16 @@ public function addConfiguration(ArrayNodeDefinition $builder) */ public function createConnectionFactory(ContainerBuilder $container, array $config) { + if (array_key_exists('driver_options', $config) && is_array($config['driver_options'])) { + $driverOptions = $config['driver_options']; + unset($config['driver_options']); + + $config = array_replace($driverOptions, $config); + } + $factory = new Definition(AmqpConnectionFactory::class); - $factory->setArguments(isset($config['dsn']) ? [$config['dsn']] : [$config]); + $factory->setFactory([self::class, 'createConnectionFactoryFactory']); + $factory->setArguments([$config]); $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); $container->setDefinition($factoryId, $factory); @@ -149,4 +182,32 @@ public function getName() { return $this->name; } + + public static function createConnectionFactoryFactory(array $config) + { + if (array_key_exists('driver', $config)) { + if ('ext' == $config['driver']) { + return new AmqpExtConnectionFactory($config); + } + if ('lib' == $config['driver']) { + return new AmqpLibConnectionFactory($config); + } + if ('bunny' == $config['driver']) { + return new AmqpBunnyConnectionFactory($config); + } + + throw new \LogicException(sprintf('Unexpected driver given "%s"', $config['driver'])); + } + + $dsn = array_key_exists('dsn', $config) ? $config['dsn'] : 'amqp:'; + $factory = dsn_to_connection_factory($dsn); + + if (false == $factory instanceof AmqpConnectionFactory) { + throw new \LogicException(sprintf('Factory must be instance of "%s" but got "%s"', AmqpConnectionFactory::class, get_class($factory))); + } + + $factoryClass = get_class($factory); + + return new $factoryClass($config); + } } diff --git a/pkg/enqueue/Symfony/DefaultTransportFactory.php b/pkg/enqueue/Symfony/DefaultTransportFactory.php index b79499c9c..24972986c 100644 --- a/pkg/enqueue/Symfony/DefaultTransportFactory.php +++ b/pkg/enqueue/Symfony/DefaultTransportFactory.php @@ -2,12 +2,6 @@ namespace Enqueue\Symfony; -use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory; -use Enqueue\AmqpBunny\Symfony\AmqpBunnyTransportFactory; -use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory; -use Enqueue\AmqpExt\Symfony\AmqpTransportFactory; -use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory; -use Enqueue\AmqpLib\Symfony\AmqpLibTransportFactory; use Enqueue\Dbal\DbalConnectionFactory; use Enqueue\Dbal\Symfony\DbalTransportFactory; use Enqueue\Fs\FsConnectionFactory; @@ -22,6 +16,7 @@ use Enqueue\Sqs\Symfony\SqsTransportFactory; use Enqueue\Stomp\StompConnectionFactory; use Enqueue\Stomp\Symfony\StompTransportFactory; +use Interop\Amqp\AmqpConnectionFactory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use function Enqueue\dsn_to_connection_factory; @@ -58,7 +53,7 @@ public function addConfiguration(ArrayNodeDefinition $builder) } if (empty($v)) { - return ['dsn' => 'null://']; + return ['dsn' => 'null:']; } if (is_string($v)) { @@ -181,16 +176,8 @@ private function findFactory($dsn) { $factory = dsn_to_connection_factory($dsn); - if ($factory instanceof AmqpExtConnectionFactory) { - return new AmqpTransportFactory('default_amqp_ext'); - } - - if ($factory instanceof AmqpLibConnectionFactory) { - return new AmqpLibTransportFactory('default_amqp_lib'); - } - - if ($factory instanceof AmqpBunnyConnectionFactory) { - return new AmqpBunnyTransportFactory('default_amqp_bunny'); + if ($factory instanceof AmqpConnectionFactory) { + return new AmqpTransportFactory('default_amqp'); } if ($factory instanceof FsConnectionFactory) { diff --git a/pkg/amqp-ext/Symfony/RabbitMqAmqpTransportFactory.php b/pkg/enqueue/Symfony/RabbitMqAmqpTransportFactory.php similarity index 98% rename from pkg/amqp-ext/Symfony/RabbitMqAmqpTransportFactory.php rename to pkg/enqueue/Symfony/RabbitMqAmqpTransportFactory.php index 8ab200e14..2bd98e584 100644 --- a/pkg/amqp-ext/Symfony/RabbitMqAmqpTransportFactory.php +++ b/pkg/enqueue/Symfony/RabbitMqAmqpTransportFactory.php @@ -1,6 +1,6 @@ assertEquals('theCustomName', $transport->getName()); } + public function testThrowIfCouldBeConstructedWithCustomName() + { + $transport = new AmqpTransportFactory('theCustomName'); + + $this->assertEquals('theCustomName', $transport->getName()); + } + public function testShouldAllowAddConfiguration() { $transport = new AmqpTransportFactory(); @@ -45,7 +52,23 @@ public function testShouldAllowAddConfiguration() $transport->addConfiguration($rootNode); $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); + $config = $processor->process($tb->buildTree(), [[ + 'host' => 'localhost', + 'port' => 5672, + 'user' => 'guest', + 'pass' => 'guest', + 'vhost' => '/', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'heartbeat' => 0, + 'persisted' => false, + 'lazy' => true, + 'qos_global' => false, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'receive_method' => 'basic_get', + ]]); $this->assertEquals([ 'host' => 'localhost', @@ -53,12 +76,42 @@ public function testShouldAllowAddConfiguration() 'user' => 'guest', 'pass' => 'guest', 'vhost' => '/', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'heartbeat' => 0, 'persisted' => false, 'lazy' => true, + 'qos_global' => false, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, 'receive_method' => 'basic_get', ], $config); } + public function testShouldAllowAddConfigurationWithDriverOptions() + { + $transport = new AmqpTransportFactory(); + $tb = new TreeBuilder(); + $rootNode = $tb->root('foo'); + + $transport->addConfiguration($rootNode); + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [[ + 'host' => 'localhost', + 'driver_options' => [ + 'foo' => 'fooVal', + ], + ]]); + + $this->assertEquals([ + 'host' => 'localhost', + 'driver_options' => [ + 'foo' => 'fooVal', + ], + ], $config); + } + public function testShouldAllowAddConfigurationAsString() { $transport = new AmqpTransportFactory(); @@ -71,14 +124,6 @@ public function testShouldAllowAddConfigurationAsString() $this->assertEquals([ 'dsn' => 'amqpDSN', - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'lazy' => true, - 'receive_method' => 'basic_get', ], $config); } @@ -111,53 +156,67 @@ public function testShouldAllowChangeReceiveMethod() ]]); $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'lazy' => true, 'receive_method' => 'basic_consume', ], $config); } - public function testShouldCreateConnectionFactory() + public function testShouldCreateConnectionFactoryForEmptyConfig() + { + $container = new ContainerBuilder(); + + $transport = new AmqpTransportFactory(); + + $serviceId = $transport->createConnectionFactory($container, []); + + $this->assertTrue($container->hasDefinition($serviceId)); + $factory = $container->getDefinition($serviceId); + $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); + + $this->assertSame([[]], $factory->getArguments()); + } + + public function testShouldCreateConnectionFactoryFromDsnString() { $container = new ContainerBuilder(); $transport = new AmqpTransportFactory(); $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, + 'dsn' => 'theConnectionDSN:', ]); $this->assertTrue($container->hasDefinition($serviceId)); $factory = $container->getDefinition($serviceId); $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]], $factory->getArguments()); + $this->assertSame([['dsn' => 'theConnectionDSN:']], $factory->getArguments()); } - public function testShouldCreateConnectionFactoryFromDsnString() + public function testShouldCreateConnectionFactoryAndMergeDriverOptionsIfSet() + { + $container = new ContainerBuilder(); + + $transport = new AmqpTransportFactory(); + + $serviceId = $transport->createConnectionFactory($container, [ + 'host' => 'aHost', + 'driver_options' => [ + 'foo' => 'fooVal', + ], + ]); + + $this->assertTrue($container->hasDefinition($serviceId)); + $factory = $container->getDefinition($serviceId); + $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); + $this->assertSame([['foo' => 'fooVal', 'host' => 'aHost']], $factory->getArguments()); + } + + public function testShouldCreateConnectionFactoryFromDsnStringPlushArrayOptions() { $container = new ContainerBuilder(); $transport = new AmqpTransportFactory(); $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theConnectionDSN', 'host' => 'localhost', 'port' => 5672, 'user' => 'guest', @@ -169,7 +228,14 @@ public function testShouldCreateConnectionFactoryFromDsnString() $this->assertTrue($container->hasDefinition($serviceId)); $factory = $container->getDefinition($serviceId); $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame(['theConnectionDSN'], $factory->getArguments()); + $this->assertSame([[ + 'host' => 'localhost', + 'port' => 5672, + 'user' => 'guest', + 'pass' => 'guest', + 'vhost' => '/', + 'persisted' => false, + ]], $factory->getArguments()); } public function testShouldCreateContext() @@ -219,4 +285,62 @@ public function testShouldCreateDriver() $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); } + + public function testShouldCreateAmqpExtConnectionFactoryBySetDriver() + { + $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'ext']); + + $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); + } + + public function testShouldCreateAmqpLibConnectionFactoryBySetDriver() + { + $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'lib']); + + $this->assertInstanceOf(\Enqueue\AmqpLib\AmqpConnectionFactory::class, $factory); + } + + public function testShouldCreateAmqpBunnyConnectionFactoryBySetDriver() + { + $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'bunny']); + + $this->assertInstanceOf(\Enqueue\AmqpBunny\AmqpConnectionFactory::class, $factory); + } + + public function testShouldCreateAmqpExtFromConfigWithoutDriverAndDsn() + { + $factory = AmqpTransportFactory::createConnectionFactoryFactory(['host' => 'aHost']); + + $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); + } + + public function testThrowIfInvalidDriverGiven() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Unexpected driver given "invalidDriver"'); + + AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'invalidDriver']); + } + + public function testShouldCreateAmqpExtFromDsn() + { + $factory = AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'amqp:']); + + $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); + } + + public function testShouldCreateAmqpBunnyFromDsnWithDriver() + { + $factory = AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'amqp+bunny:']); + + $this->assertInstanceOf(\Enqueue\AmqpBunny\AmqpConnectionFactory::class, $factory); + } + + public function testThrowIfNotAmqpDsnProvided() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Factory must be instance of "Interop\Amqp\AmqpConnectionFactory" but got "Enqueue\Sqs\SqsConnectionFactory"'); + + AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'sqs:']); + } } diff --git a/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php index 5ee18c211..7f84d6d04 100644 --- a/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php +++ b/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php @@ -82,10 +82,10 @@ public function testShouldSetNullTransportByDefault() $processor = new Processor(); $config = $processor->process($tb->buildTree(), [null]); - $this->assertEquals(['dsn' => 'null://'], $config); + $this->assertEquals(['dsn' => 'null:'], $config); $config = $processor->process($tb->buildTree(), ['']); - $this->assertEquals(['dsn' => 'null://'], $config); + $this->assertEquals(['dsn' => 'null:'], $config); } public function testThrowIfNeitherDsnNorAliasConfigured() @@ -250,19 +250,19 @@ public function testShouldCreateDriverFromDsn($dsn, $expectedName) public static function provideDSNs() { - yield ['amqp+ext://', 'default_amqp_ext']; + yield ['amqp+ext:', 'default_amqp']; - yield ['amqp+lib:', 'default_amqp_lib']; + yield ['amqp+lib:', 'default_amqp']; - yield ['amqp+bunny://', 'default_amqp_bunny']; + yield ['amqp+bunny:', 'default_amqp']; - yield ['null://', 'default_null']; + yield ['null:', 'default_null']; - yield ['file://', 'default_fs']; + yield ['file:', 'default_fs']; - yield ['mysql://', 'default_dbal']; + yield ['mysql:', 'default_dbal']; - yield ['pgsql://', 'default_dbal']; + yield ['pgsql:', 'default_dbal']; yield ['gps:', 'default_gps']; diff --git a/pkg/amqp-ext/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php similarity index 90% rename from pkg/amqp-ext/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php rename to pkg/enqueue/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php index 31853492a..cfe77e6d4 100644 --- a/pkg/amqp-ext/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php +++ b/pkg/enqueue/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php @@ -1,13 +1,13 @@ process($tb->buildTree(), []); $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, 'delay_strategy' => 'dlx', - 'lazy' => true, - 'receive_method' => 'basic_get', ], $config); } diff --git a/pkg/enqueue/composer.json b/pkg/enqueue/composer.json index 334980577..638efea94 100644 --- a/pkg/enqueue/composer.json +++ b/pkg/enqueue/composer.json @@ -7,7 +7,7 @@ "require": { "php": ">=5.6", "queue-interop/queue-interop": "^0.6@dev", - "enqueue/null": "^0.7@dev", + "enqueue/null": "^0.8@dev", "ramsey/uuid": "^2|^3.5", "psr/log": "^1" }, @@ -18,20 +18,20 @@ "symfony/config": "^2.8|^3", "symfony/event-dispatcher": "^2.8|^3", "symfony/http-kernel": "^2.8|^3", - "enqueue/amqp-ext": "^0.7@dev", - "enqueue/amqp-lib": "^0.7@dev", - "enqueue/amqp-bunny": "^0.7@dev", - "enqueue/pheanstalk": "^0.7@dev", - "enqueue/gearman": "^0.7@dev", - "enqueue/rdkafka": "^0.7@dev", - "enqueue/dbal": "^0.7@dev", - "enqueue/fs": "^0.7@dev", - "enqueue/gps": "^0.7@dev", - "enqueue/redis": "^0.7@dev", - "enqueue/sqs": "^0.7@dev", - "enqueue/stomp": "^0.7@dev", - "enqueue/test": "^0.7@dev", - "enqueue/simple-client": "^0.7@dev", + "enqueue/amqp-ext": "^0.8@dev", + "enqueue/amqp-lib": "^0.8@dev", + "enqueue/amqp-bunny": "^0.8@dev", + "enqueue/pheanstalk": "^0.8@dev", + "enqueue/gearman": "^0.8@dev", + "enqueue/rdkafka": "^0.8@dev", + "enqueue/dbal": "^0.8@dev", + "enqueue/fs": "^0.8@dev", + "enqueue/gps": "^0.8@dev", + "enqueue/redis": "^0.8@dev", + "enqueue/sqs": "^0.8@dev", + "enqueue/stomp": "^0.8@dev", + "enqueue/test": "^0.8@dev", + "enqueue/simple-client": "^0.8@dev", "empi89/php-amqp-stubs": "*@dev" }, "suggest": { @@ -55,7 +55,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/fs/FsConnectionFactory.php b/pkg/fs/FsConnectionFactory.php index f155c9df9..8627edb95 100644 --- a/pkg/fs/FsConnectionFactory.php +++ b/pkg/fs/FsConnectionFactory.php @@ -23,14 +23,15 @@ class FsConnectionFactory implements PsrConnectionFactory * * or * + * file: - create queue files in tmp dir. * file://home/foo/enqueue * file://home/foo/enqueue?pre_fetch_count=20&chmod=0777 * * @param array|string|null $config */ - public function __construct($config = 'file://') + public function __construct($config = 'file:') { - if (empty($config) || 'file://' === $config) { + if (empty($config) || 'file:' === $config) { $config = ['path' => sys_get_temp_dir().'/enqueue']; } elseif (is_string($config)) { $config = $this->parseDsn($config); @@ -68,8 +69,8 @@ private function parseDsn($dsn) return ['path' => $dsn]; } - if (false === strpos($dsn, 'file://')) { - throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "file://".', $dsn)); + if (false === strpos($dsn, 'file:')) { + throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "file:".', $dsn)); } $dsn = substr($dsn, 7); diff --git a/pkg/fs/FsConsumer.php b/pkg/fs/FsConsumer.php index 1bd36c498..c42ea4dfc 100644 --- a/pkg/fs/FsConsumer.php +++ b/pkg/fs/FsConsumer.php @@ -119,6 +119,15 @@ public function receiveNoWait() $count = $this->preFetchCount; while ($count) { $frame = $this->readFrame($file, 1); + + //guards + if ($frame && false == ('|' == $frame[0] || ' ' == $frame[0])) { + throw new \LogicException(sprintf('The frame could start from either " " or "|". The malformed frame starts with "%s".', $frame[0])); + } + if (0 !== $reminder = strlen($frame) % 64) { + throw new \LogicException(sprintf('The frame size is "%d" and it must divide exactly to 64 but it leaves a reminder "%d".', strlen($frame), $reminder)); + } + ftruncate($file, fstat($file)['size'] - strlen($frame)); rewind($file); @@ -212,7 +221,12 @@ private function readFrame($file, $frameNumber) $previousFrame = $this->readFrame($file, $frameNumber + 1); if ('|' === substr($previousFrame, -1) && '{' === $frame[0]) { - return '|'.$frame; + $matched = []; + if (false === preg_match('/\ *?\|$/', $previousFrame, $matched)) { + throw new \LogicException('Something went completely wrong.'); + } + + return $matched[0].$frame; } return $previousFrame.$frame; diff --git a/pkg/fs/Tests/FsConnectionFactoryConfigTest.php b/pkg/fs/Tests/FsConnectionFactoryConfigTest.php index a42ede8c6..c79581c51 100644 --- a/pkg/fs/Tests/FsConnectionFactoryConfigTest.php +++ b/pkg/fs/Tests/FsConnectionFactoryConfigTest.php @@ -24,7 +24,7 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() public function testThrowIfSchemeIsNotAmqp() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN "http://example.com" is not supported. Must start with "file://'); + $this->expectExceptionMessage('The given DSN "http://example.com" is not supported. Must start with "file:'); new FsConnectionFactory('http://example.com'); } @@ -83,7 +83,7 @@ public static function provideConfigs() ]; yield [ - 'file://', + 'file:', [ 'path' => sys_get_temp_dir().'/enqueue', 'pre_fetch_count' => 1, diff --git a/pkg/fs/Tests/FsConnectionFactoryTest.php b/pkg/fs/Tests/FsConnectionFactoryTest.php index aa7c511bd..30cfc6f69 100644 --- a/pkg/fs/Tests/FsConnectionFactoryTest.php +++ b/pkg/fs/Tests/FsConnectionFactoryTest.php @@ -19,7 +19,7 @@ public function testShouldImplementConnectionFactoryInterface() public function testShouldCreateContext() { $factory = new FsConnectionFactory([ - 'path' => 'theDir', + 'path' => __DIR__, 'pre_fetch_count' => 123, 'chmod' => 0765, ]); @@ -28,7 +28,7 @@ public function testShouldCreateContext() $this->assertInstanceOf(FsContext::class, $context); - $this->assertAttributeSame('theDir', 'storeDir', $context); + $this->assertAttributeSame(__DIR__, 'storeDir', $context); $this->assertAttributeSame(123, 'preFetchCount', $context); $this->assertAttributeSame(0765, 'chmod', $context); } diff --git a/pkg/fs/Tests/Functional/FsConsumerTest.php b/pkg/fs/Tests/Functional/FsConsumerTest.php index 33e676ae4..f3a641159 100644 --- a/pkg/fs/Tests/Functional/FsConsumerTest.php +++ b/pkg/fs/Tests/Functional/FsConsumerTest.php @@ -4,6 +4,7 @@ use Enqueue\Fs\FsConnectionFactory; use Enqueue\Fs\FsContext; +use Enqueue\Fs\FsDestination; use Enqueue\Fs\FsMessage; use PHPUnit\Framework\TestCase; @@ -69,6 +70,7 @@ public function testShouldConsumeMessagesFromFileOneByOne() /** * @group bug + * @group bug170 */ public function testShouldNotFailOnSpecificMessageSize() { @@ -91,4 +93,82 @@ public function testShouldNotFailOnSpecificMessageSize() $message = $consumer->receiveNoWait(); $this->assertNull($message); } + + /** + * @group bug + * @group bug170 + */ + public function testShouldNotCorruptFrameSize() + { + $context = $this->fsContext; + $queue = $context->createQueue('fs_test_queue'); + $context->purge($queue); + + $consumer = $context->createConsumer($queue); + $producer = $context->createProducer(); + + $producer->send($queue, $context->createMessage(str_repeat('a', 23))); + $producer->send($queue, $context->createMessage(str_repeat('b', 24))); + + $message = $consumer->receiveNoWait(); + $this->assertNotNull($message); + $context->workWithFile($queue, 'a+', function (FsDestination $destination, $file) { + $this->assertSame(0, fstat($file)['size'] % 64); + }); + + $message = $consumer->receiveNoWait(); + $this->assertNotNull($message); + $context->workWithFile($queue, 'a+', function (FsDestination $destination, $file) { + $this->assertSame(0, fstat($file)['size'] % 64); + }); + + $message = $consumer->receiveNoWait(); + $this->assertNull($message); + } + + /** + * @group bug + * @group bug202 + */ + public function testShouldThrowExceptionForTheCorruptedQueueFile() + { + $context = $this->fsContext; + $queue = $context->createQueue('fs_test_queue'); + $context->purge($queue); + + $context->workWithFile($queue, 'a+', function (FsDestination $destination, $file) { + fwrite($file, '|{"body":"{\"path\":\"\\\/p\\\/r\\\/pr_swoppad_6_4910_red_1.jpg\",\"filters\":null,\"force\":false}","properties":{"enqueue.topic_name":"liip_imagine_resolve_cache"},"headers":{"content_type":"application\/json","message_id":"46fdc345-5d0c-426e-95ac-227c7e657839","timestamp":1505379216,"reply_to":null,"correlation_id":""}} |{"body":"{\"path\":\"\\\/p\\\/r\\\/pr_swoppad_6_4910_black_1.jpg\",\"filters\":null,\"force\":false}","properties":{"enqueue.topic_name":"liip_imagine_resolve_cache"},"headers":{"content_type":"application\/json","message_id":"c4d60e39-3a8c-42df-b536-c8b7c13e006d","timestamp":1505379216,"reply_to":null,"correlation_id":""}} |{"body":"{\"path\":\"\\\/p\\\/r\\\/pr_swoppad_6_4910_green_1.jpg\",\"filters\":null,\"force\":false}","properties":{"enqueue.topic_name":"liip_imagine_resolve_cache"},"headers":{"content_type":"application\/json","message_id":"3a6aa176-c879-4435-9626-c48e0643defa","timestamp":1505379216,"reply_to":null,"correlation_id":""}}'); + }); + + $consumer = $context->createConsumer($queue); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The frame could start from either " " or "|". The malformed frame starts with """.'); + $consumer->receiveNoWait(); + } + + /** + * @group bug + * @group bug202 + */ + public function testShouldThrowExceptionWhenFrameSizeNotDivideExactly() + { + $context = $this->fsContext; + $queue = $context->createQueue('fs_test_queue'); + $context->purge($queue); + + $context->workWithFile($queue, 'a+', function (FsDestination $destination, $file) { + $msg = '|{"body":""}'; + //guard + $this->assertNotSame(0, strlen($msg) % 64); + + fwrite($file, $msg); + }); + + $consumer = $context->createConsumer($queue); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The frame size is "12" and it must divide exactly to 64 but it leaves a reminder "12".'); + $consumer->receiveNoWait(); + } } diff --git a/pkg/fs/composer.json b/pkg/fs/composer.json index 5494b705e..a76cbaca8 100644 --- a/pkg/fs/composer.json +++ b/pkg/fs/composer.json @@ -15,7 +15,7 @@ "enqueue/enqueue": "^0.7", "enqueue/null": "^0.7", "enqueue/test": "^0.7", - "queue-interop/queue-spec": "^0.5", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3@stable", "symfony/config": "^2.8|^3@stable", "symfony/phpunit-bridge": "^2.8|^3@stable" @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/gearman/GearmanConnectionFactory.php b/pkg/gearman/GearmanConnectionFactory.php index dd91dccaa..77f8dfd96 100644 --- a/pkg/gearman/GearmanConnectionFactory.php +++ b/pkg/gearman/GearmanConnectionFactory.php @@ -25,9 +25,9 @@ class GearmanConnectionFactory implements PsrConnectionFactory * * @param array|string $config */ - public function __construct($config = 'gearman://') + public function __construct($config = 'gearman:') { - if (empty($config) || 'gearman://' === $config) { + if (empty($config) || 'gearman:' === $config) { $config = []; } elseif (is_string($config)) { $config = $this->parseDsn($config); diff --git a/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php b/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php index 75c61591e..bb8ab7ad6 100644 --- a/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php +++ b/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php @@ -62,7 +62,7 @@ public static function provideConfigs() ]; yield [ - 'gearman://', + 'gearman:', [ 'host' => 'localhost', 'port' => 4730, diff --git a/pkg/gearman/composer.json b/pkg/gearman/composer.json index a26d3f984..4529a1197 100644 --- a/pkg/gearman/composer.json +++ b/pkg/gearman/composer.json @@ -11,10 +11,10 @@ }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -30,7 +30,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/gps/composer.json b/pkg/gps/composer.json index ae3d50aa0..b9540a51c 100644 --- a/pkg/gps/composer.json +++ b/pkg/gps/composer.json @@ -11,9 +11,9 @@ }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/job-queue/composer.json b/pkg/job-queue/composer.json index 1d0d57895..816c5c164 100644 --- a/pkg/job-queue/composer.json +++ b/pkg/job-queue/composer.json @@ -7,13 +7,13 @@ "require": { "php": ">=5.6", "symfony/framework-bundle": "^2.8|^3", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", "doctrine/orm": "~2.4" }, "require-dev": { "phpunit/phpunit": "~5.5", - "enqueue/test": "^0.7@dev", + "enqueue/test": "^0.8@dev", "doctrine/doctrine-bundle": "~1.2", "symfony/browser-kit": "^2.8|^3", "symfony/expression-language": "^2.8|^3" @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/null/composer.json b/pkg/null/composer.json index 58bdafcb3..e24c54c2f 100644 --- a/pkg/null/composer.json +++ b/pkg/null/composer.json @@ -10,9 +10,9 @@ }, "require-dev": { "phpunit/phpunit": "~5.5", - "enqueue/enqueue": "^0.7@dev", - "enqueue/test": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/test": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/pheanstalk/PheanstalkConnectionFactory.php b/pkg/pheanstalk/PheanstalkConnectionFactory.php index c2d629769..40999ccef 100644 --- a/pkg/pheanstalk/PheanstalkConnectionFactory.php +++ b/pkg/pheanstalk/PheanstalkConnectionFactory.php @@ -29,13 +29,14 @@ class PheanstalkConnectionFactory implements PsrConnectionFactory * * or * + * beanstalk: - connects to localhost:11300 * beanstalk://host:port * * @param array|string $config */ - public function __construct($config = 'beanstalk://') + public function __construct($config = 'beanstalk:') { - if (empty($config) || 'beanstalk://' === $config) { + if (empty($config) || 'beanstalk:' === $config) { $config = []; } elseif (is_string($config)) { $config = $this->parseDsn($config); diff --git a/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php b/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php index 239dae68f..231ec0d77 100644 --- a/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php +++ b/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php @@ -63,7 +63,7 @@ public static function provideConfigs() ]; yield [ - 'beanstalk://', + 'beanstalk:', [ 'host' => 'localhost', 'port' => 11300, diff --git a/pkg/pheanstalk/composer.json b/pkg/pheanstalk/composer.json index 2c3f58f77..542e18ae5 100644 --- a/pkg/pheanstalk/composer.json +++ b/pkg/pheanstalk/composer.json @@ -11,10 +11,10 @@ }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -30,7 +30,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/rdkafka/RdKafkaConnectionFactory.php b/pkg/rdkafka/RdKafkaConnectionFactory.php index 4db818686..a3fdbc13e 100644 --- a/pkg/rdkafka/RdKafkaConnectionFactory.php +++ b/pkg/rdkafka/RdKafkaConnectionFactory.php @@ -29,13 +29,13 @@ class RdKafkaConnectionFactory implements PsrConnectionFactory * * or * - * rdkafka://host:port + * kafka://host:port * * @param array|string $config */ - public function __construct($config = 'rdkafka://') + public function __construct($config = 'kafka:') { - if (empty($config) || 'rdkafka://' === $config) { + if (empty($config) || 'kafka:' === $config) { $config = []; } elseif (is_string($config)) { $config = $this->parseDsn($config); @@ -79,8 +79,8 @@ private function parseDsn($dsn) 'query' => null, ], $dsnConfig); - if ('rdkafka' !== $dsnConfig['scheme']) { - throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be "rdkafka" only.', $dsnConfig['scheme'])); + if ('kafka' !== $dsnConfig['scheme']) { + throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be "kafka" only.', $dsnConfig['scheme'])); } $config = []; diff --git a/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php b/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php index 5b7f82bfe..01f653ee6 100644 --- a/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php +++ b/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php @@ -21,7 +21,7 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() public function testThrowIfSchemeIsNotBeanstalkAmqp() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be "rdkafka" only.'); + $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be "kafka" only.'); new RdKafkaConnectionFactory('http://example.com'); } @@ -29,9 +29,9 @@ public function testThrowIfSchemeIsNotBeanstalkAmqp() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "rdkafka://:@/"'); + $this->expectExceptionMessage('Failed to parse DSN "kafka://:@/"'); - new RdKafkaConnectionFactory('rdkafka://:@/'); + new RdKafkaConnectionFactory('kafka://:@/'); } public function testShouldBeExpectedDefaultConfig() @@ -53,7 +53,7 @@ public function testShouldBeExpectedDefaultConfig() public function testShouldBeExpectedDefaultDsnConfig() { - $factory = new RdKafkaConnectionFactory('rdkafka://'); + $factory = new RdKafkaConnectionFactory('kafka:'); $config = $this->getObjectAttribute($factory, 'config'); @@ -84,7 +84,7 @@ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) public static function provideConfigs() { yield [ - 'rdkafka://theHost:1234?global%5Bgroup.id%5D=group-id', + 'kafka://theHost:1234?global%5Bgroup.id%5D=group-id', [ 'global' => [ 'metadata.broker.list' => 'theHost:1234', diff --git a/pkg/rdkafka/composer.json b/pkg/rdkafka/composer.json index a50392ed6..1014002d1 100644 --- a/pkg/rdkafka/composer.json +++ b/pkg/rdkafka/composer.json @@ -11,10 +11,10 @@ }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "kwn/php-rdkafka-stubs": "^1.0.2" }, "autoload": { @@ -40,7 +40,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/redis/composer.json b/pkg/redis/composer.json index 97b138ad0..cb924e286 100644 --- a/pkg/redis/composer.json +++ b/pkg/redis/composer.json @@ -11,10 +11,10 @@ "require-dev": { "phpunit/phpunit": "~5.4.0", "predis/predis": "^1.1", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/simple-client/SimpleClient.php b/pkg/simple-client/SimpleClient.php index cd7dc0793..5bdc4f1c3 100644 --- a/pkg/simple-client/SimpleClient.php +++ b/pkg/simple-client/SimpleClient.php @@ -2,8 +2,6 @@ namespace Enqueue\SimpleClient; -use Enqueue\AmqpExt\Symfony\AmqpTransportFactory; -use Enqueue\AmqpExt\Symfony\RabbitMqAmqpTransportFactory; use Enqueue\Client\ArrayProcessorRegistry; use Enqueue\Client\Config; use Enqueue\Client\DelegateProcessor; @@ -22,7 +20,9 @@ use Enqueue\Sqs\Symfony\SqsTransportFactory; use Enqueue\Stomp\Symfony\RabbitMqStompTransportFactory; use Enqueue\Stomp\Symfony\StompTransportFactory; +use Enqueue\Symfony\AmqpTransportFactory; use Enqueue\Symfony\DefaultTransportFactory; +use Enqueue\Symfony\RabbitMqAmqpTransportFactory; use Interop\Queue\PsrContext; use Interop\Queue\PsrProcessor; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -37,10 +37,10 @@ final class SimpleClient /** * The config could be a transport DSN (string) or an array, here's an example of a few DSNs:. * - * amqp:// + * amqp: * amqp://guest:guest@localhost:5672/%2f?lazy=1&persisted=1 * file://foo/bar/ - * null:// + * null: * * or an array, the most simple: * @@ -275,8 +275,6 @@ private function buildContainerExtension() { $map = [ 'default' => DefaultTransportFactory::class, - 'amqp' => AmqpTransportFactory::class, - 'rabbitmq_amqp' => RabbitMqAmqpTransportFactory::class, 'dbal' => DbalTransportFactory::class, 'fs' => FsTransportFactory::class, 'redis' => RedisTransportFactory::class, @@ -293,6 +291,9 @@ private function buildContainerExtension() } } + $extension->addTransportFactory(new AmqpTransportFactory('amqp')); + $extension->addTransportFactory(new RabbitMqAmqpTransportFactory('rabbitmq_amqp')); + return $extension; } diff --git a/pkg/simple-client/Tests/Functional/SimpleClientTest.php b/pkg/simple-client/Tests/Functional/SimpleClientTest.php index 35dfd979c..3281aa274 100644 --- a/pkg/simple-client/Tests/Functional/SimpleClientTest.php +++ b/pkg/simple-client/Tests/Functional/SimpleClientTest.php @@ -35,6 +35,7 @@ public function transportConfigDataProvider() 'transport' => [ 'default' => 'amqp', 'amqp' => [ + 'driver' => 'ext', 'host' => getenv('SYMFONY__RABBITMQ__HOST'), 'port' => getenv('SYMFONY__RABBITMQ__AMQP__PORT'), 'user' => getenv('SYMFONY__RABBITMQ__USER'), @@ -63,6 +64,7 @@ public function transportConfigDataProvider() 'transport' => [ 'default' => 'rabbitmq_amqp', 'rabbitmq_amqp' => [ + 'driver' => 'ext', 'host' => getenv('SYMFONY__RABBITMQ__HOST'), 'port' => getenv('SYMFONY__RABBITMQ__AMQP__PORT'), 'user' => getenv('SYMFONY__RABBITMQ__USER'), diff --git a/pkg/simple-client/composer.json b/pkg/simple-client/composer.json index 38a3a8428..4f1c737ab 100644 --- a/pkg/simple-client/composer.json +++ b/pkg/simple-client/composer.json @@ -6,17 +6,17 @@ "license": "MIT", "require": { "php": ">=5.6", - "enqueue/enqueue": "^0.7@dev", + "enqueue/enqueue": "^0.8@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3", "symfony/console": "^2.8|^3" }, "require-dev": { "phpunit/phpunit": "~5.5", - "enqueue/test": "^0.7@dev", - "enqueue/amqp-ext": "^0.7@dev", - "enqueue/fs": "^0.7@dev", - "enqueue/null": "^0.7@dev" + "enqueue/test": "^0.8@dev", + "enqueue/amqp-ext": "^0.8@dev", + "enqueue/fs": "^0.8@dev", + "enqueue/null": "^0.8@dev" }, "autoload": { "psr-4": { "Enqueue\\SimpleClient\\": "" }, @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/sqs/composer.json b/pkg/sqs/composer.json index 7598022db..c8e3a51c9 100644 --- a/pkg/sqs/composer.json +++ b/pkg/sqs/composer.json @@ -11,9 +11,9 @@ }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/stomp/composer.json b/pkg/stomp/composer.json index d071535f3..703cc9fe0 100644 --- a/pkg/stomp/composer.json +++ b/pkg/stomp/composer.json @@ -13,10 +13,10 @@ }, "require-dev": { "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.7@dev", - "enqueue/enqueue": "^0.7@dev", - "enqueue/null": "^0.7@dev", - "queue-interop/queue-spec": "^0.5@dev", + "enqueue/test": "^0.8@dev", + "enqueue/enqueue": "^0.8@dev", + "enqueue/null": "^0.8@dev", + "queue-interop/queue-spec": "^0.5.3@dev", "symfony/dependency-injection": "^2.8|^3", "symfony/config": "^2.8|^3" }, @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } diff --git a/pkg/test/composer.json b/pkg/test/composer.json index 5c89bfd2e..8d25ea853 100644 --- a/pkg/test/composer.json +++ b/pkg/test/composer.json @@ -7,7 +7,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } }