Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Latest
------

### Changes
* [#52](https://github.com/cleverage/soap-process-bundle/issues/52) ClientRegistry: the clients are loaded lazily from a service locator indexed by code: a client is only instantiated when a task or the transformer uses it (an error in a client that a process does not use no longer makes it fail). The code is given by the new `code` attribute of the `cleverage.soap.client` tag, or by the new `#[AsSoapClient(code: '...')]` attribute (autoconfiguration); it must be the one returned by `getCode()`. Duplicate or empty codes fail the container compilation. A client tagged without `code` is still registered eagerly, with `ClientRegistry::addClient()`, which is deprecated (the `code` will be required in 4.0). New optional `ClientRegistry` constructor argument (the locator); `ClientRegistry` is autowirable by its class name. Require `symfony/service-contracts`. Update documentation, add tests.
* [#50](https://github.com/cleverage/soap-process-bundle/issues/50) Require `symfony/polyfill-php83`, `symfony/polyfill-php84` and `symfony/polyfill-php85` ([cleverage/process-bundle#299](https://github.com/cleverage/process-bundle/issues/299)), so the functions of PHP 8.3 to 8.5 can be used while supporting PHP 8.2. Configure Rector for PHP 8.2 with its polyfills set (it was configured for PHP 8.5, and could introduce PHP 8.3+ functions without polyfill). No code change: no Rector rule applies and no manual replacement keeps an identical behaviour. Document it in `CONTRIBUTING.md`.
* [#46](https://github.com/cleverage/soap-process-bundle/issues/46) Give a code to the task ([cleverage/process-bundle#91](https://github.com/cleverage/process-bundle/issues/91)): `soap_request` (`task: soap_request`, with `cleverage/process-bundle` 5.4 or later, referencing it by `service` being deprecated there). The task has its class name as service id; fetching it directly from the service container is deprecated (`container.private`), it will be private in 4.0. The former id `cleverage_soap_process.task.request` is a deprecated alias, removed in 4.0. Update documentation, add tests.
* [#45](https://github.com/cleverage/soap-process-bundle/issues/45) Tag the `RequestTransformer` with its code, `soap_request`, for its lazy loading by `cleverage/process-bundle` 5.4 or later ([cleverage/process-bundle#276](https://github.com/cleverage/process-bundle/issues/276)). The transformer has its class name as service id; fetching it directly from the service container is deprecated (`container.private`), it will be private in 4.0. The former id `cleverage_soap_process.transformer.request` is a deprecated alias, removed in 4.0. Update documentation, add tests.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"symfony/options-resolver": "^6.4|^7.4|^8",
"symfony/polyfill-php83": "^1.33",
"symfony/polyfill-php84": "^1.33",
"symfony/polyfill-php85": "^1.33"
"symfony/polyfill-php85": "^1.33",
"symfony/service-contracts": "^2.5 || ^3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
Expand Down
6 changes: 6 additions & 0 deletions config/services/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ services:
cleverage_soap_process.registry.client:
class: CleverAge\SoapProcessBundle\Registry\ClientRegistry
public: false
arguments:
- ~ # Service locator of the clients by code, see RegisterClientsPass

CleverAge\SoapProcessBundle\Registry\ClientRegistry:
alias: cleverage_soap_process.registry.client
public: false
7 changes: 4 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ CleverAge\SoapProcessBundle\CleverAgeSoapProcessBundle::class => ['all' => true]

## Configuration

The bundle has no configuration. Declare at least one SOAP client as a service tagged `cleverage.soap.client`, its
`code` is then used by the `client` option of the task and the transformer (see [Client](reference/client.md)):
The bundle has no configuration. Declare at least one SOAP client as a service tagged `cleverage.soap.client` with its
`code` (or with the `#[AsSoapClient]` attribute), which is then used by the `client` option of the task and the
transformer (see [Client](reference/client.md)):

```yaml
# config/services.yaml
Expand All @@ -38,7 +39,7 @@ services:
exceptions: true
features: !php/const SOAP_SINGLE_ELEMENT_ARRAYS
tags:
- { name: cleverage.soap.client }
- { name: cleverage.soap.client, code: country_info }
```

The task is registered as a service (non-shared) and is used in your processes by its code, `soap_request` (e.g.
Expand Down
25 changes: 17 additions & 8 deletions docs/reference/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ Client reference

* **Interface**: `CleverAge\SoapProcessBundle\Client\ClientInterface`
* **Base class**: `CleverAge\SoapProcessBundle\Client\Client`
* **Service tag**: `cleverage.soap.client`, every tagged service is registered in the
`cleverage_soap_process.registry.client` registry (`CleverAge\SoapProcessBundle\Registry\ClientRegistry`)
* **Service tag**: `cleverage.soap.client`, with a `code` attribute (`- { name: cleverage.soap.client, code: my_api }`):
every tagged service is registered in the `cleverage_soap_process.registry.client` registry
(`CleverAge\SoapProcessBundle\Registry\ClientRegistry`, autowirable by its class name)
* **Attribute**: `CleverAge\SoapProcessBundle\Attribute\AsSoapClient` (`#[AsSoapClient(code: 'my_api')]`, with
autoconfiguration), equivalent to the tag

The bundle does not declare any client: you have to register at least one service, either with the
`CleverAge\SoapProcessBundle\Client\Client` class or with your own implementation of `ClientInterface`.
Expand Down Expand Up @@ -66,7 +69,7 @@ services:
cache_wsdl: !php/const WSDL_CACHE_BOTH
features: !php/const SOAP_SINGLE_ELEMENT_ARRAYS
tags:
- { name: cleverage.soap.client }
- { name: cleverage.soap.client, code: country_info }
```

With `autowire: true` in the `_defaults` of your `config/services.yaml`, the `$logger` argument can be omitted.
Expand All @@ -88,7 +91,7 @@ services:
login: '%env(ERP_SOAP_LOGIN)%'
password: '%env(ERP_SOAP_PASSWORD)%'
tags:
- { name: cleverage.soap.client }
- { name: cleverage.soap.client, code: legacy_erp }
```

* Custom client, overriding the call of one SOAP method
Expand Down Expand Up @@ -122,10 +125,16 @@ class CountryInfoClient extends Client
Notes
-----

* Codes must be unique: registering two clients with the same code throws an `\UnexpectedValueException` giving the
ids of both services (`Client <code> is already defined by service "<id>", cannot register service "<id>"`) when
the registry is instantiated, i.e. when the first SOAP task or
transformer service is built.
* The clients are registered by the `code` attribute of their tag, in a service locator built when the container is
compiled: a client is only instantiated when a task or the transformer uses it (an error in a client that a process
does not use does not make it fail). The `code` must be the one returned by `getCode()` (an
`\UnexpectedValueException` is thrown otherwise when it is fetched). Two clients with the same code, or an empty
code, fail the container compilation.
* **Deprecated**: a service tagged without `code` is still registered, with `ClientRegistry::addClient()`, instantiated
with the registry (deprecated since 3.3, the `code` will be required in 4.0). Two clients with the same code then
throw an `\UnexpectedValueException` giving the ids of both services
(`Client <code> is already defined by service "<id>", cannot register service "<id>"`) when the registry is
instantiated, i.e. when the first SOAP task or transformer service is built.
* Using a code that is not registered throws a `CleverAge\SoapProcessBundle\Exception\MissingClientException`
(`No Soap client with code : <code>`).
* The `SoapClient` is created lazily, on the first call, and then reused: the WSDL is only loaded once per client.
Expand Down
29 changes: 29 additions & 0 deletions src/Attribute/AsSoapClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the CleverAge/SoapProcessBundle package.
*
* Copyright (c) Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\SoapProcessBundle\Attribute;

/**
* Registers a SOAP client service under its code, so that it is only instantiated when a process uses it.
*
* Requires autoconfiguration; equivalent to a `cleverage.soap.client` tag with a `code` attribute. The code must be
* the one returned by `ClientInterface::getCode()`.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final readonly class AsSoapClient
{
public function __construct(
public string $code,
) {
}
}
9 changes: 9 additions & 0 deletions src/DependencyInjection/CleverAgeSoapProcessExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

namespace CleverAge\SoapProcessBundle\DependencyInjection;

use CleverAge\SoapProcessBundle\Attribute\AsSoapClient;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
Expand All @@ -29,6 +31,13 @@ class CleverAgeSoapProcessExtension extends Extension
public function load(array $configs, ContainerBuilder $container): void
{
$this->findServices($container, __DIR__.'/../../config/services');

$container->registerAttributeForAutoconfiguration(
AsSoapClient::class,
static function (ChildDefinition $definition, AsSoapClient $attribute): void {
$definition->addTag('cleverage.soap.client', ['code' => $attribute->code]);
}
);
}

/**
Expand Down
36 changes: 32 additions & 4 deletions src/DependencyInjection/Compiler/RegisterClientsPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
namespace CleverAge\SoapProcessBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
* Adds the tagged SOAP clients to the client registry, with their service id.
* Registers the `cleverage.soap.client` tagged services in the client registry: in a service locator indexed by the
* `code` attribute of the tag (lazy), or, without `code` (deprecated), with an addClient() call (eager).
*/
class RegisterClientsPass implements CompilerPassInterface
{
Expand All @@ -27,10 +30,35 @@ public function process(ContainerBuilder $container): void
if (!$container->has('cleverage_soap_process.registry.client')) {
return;
}

$definition = $container->findDefinition('cleverage_soap_process.registry.client');
foreach (array_keys($container->findTaggedServiceIds('cleverage.soap.client')) as $id) {
$definition->addMethodCall('addClient', [new Reference($id), $id]);

$services = [];
$ids = [];
foreach ($container->findTaggedServiceIds('cleverage.soap.client') as $id => $tags) {
$codes = [];
foreach ($tags as $attributes) {
if (!\is_array($attributes) || !isset($attributes['code'])) {
continue;
}
if (!\is_string($attributes['code']) || '' === $attributes['code']) {
throw new InvalidArgumentException(\sprintf('The "code" attribute of the "cleverage.soap.client" tag of the "%s" service must be a non-empty string.', $id));
}
$codes[] = $attributes['code'];
}
if ([] === $codes) {
// Deprecated: instantiated with the registry, addClient() triggers the deprecation
$definition->addMethodCall('addClient', [new Reference($id), $id]);
continue;
}
foreach (array_unique($codes) as $code) {
if (isset($ids[$code])) {
throw new InvalidArgumentException(\sprintf('The SOAP client code "%s" is used by the "%s" and "%s" services.', $code, $ids[$code], $id));
}
$ids[$code] = $id;
$services[$code] = new Reference($id);
}
}

$definition->setArgument(0, ServiceLocatorTagPass::register($container, $services));
}
}
66 changes: 56 additions & 10 deletions src/Registry/ClientRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,59 @@

use CleverAge\SoapProcessBundle\Client\ClientInterface;
use CleverAge\SoapProcessBundle\Exception\MissingClientException;
use Symfony\Contracts\Service\ServiceProviderInterface;

/**
* Holds all tagged soap client services.
* Holds the SOAP clients, indexed by code.
*
* The client services are fetched lazily from a service locator indexed by code (`code` attribute of the
* `cleverage.soap.client` tag, or `#[AsSoapClient]`, see RegisterClientsPass): a client is only instantiated when it
* is used.
*
* @author Madeline Veyrenc <[email protected]>
*/
class ClientRegistry
{
/** @var ClientInterface[] */
/**
* Clients already fetched from the locator, and the ones registered with addClient() (deprecated).
*
* @var array<string, ClientInterface>
*/
private array $clients = [];

/** @var array<string, string|null> Service ids of the clients, indexed by code */
/** @var array<string, string|null> Service ids of the clients registered with addClient(), indexed by code */
private array $serviceIds = [];

/**
* @param ServiceProviderInterface<mixed>|null $locator Client services, checked when fetched
*/
public function __construct(
private readonly ?ServiceProviderInterface $locator = null,
) {
}

/**
* @param string|null $serviceId Id of the client service, used to identify the clients with the same code
*
* @deprecated since 3.3, tag the client service with a "code" attribute or use #[AsSoapClient] instead
*/
public function addClient(ClientInterface $client, ?string $serviceId = null): void
{
$code = $client->getCode();
if (\array_key_exists($code, $this->getClients())) {
trigger_deprecation(
'cleverage/soap-process-bundle',
'3.3',
'Registering the SOAP client "%s" (%s) with "%s()" is deprecated, tag its service with a "code" attribute ({ name: cleverage.soap.client, code: %s }) or use the #[AsSoapClient] attribute instead, so that it is only instantiated when used. It will be required in 4.0.',
$code,
$client::class,
__METHOD__,
$code,
);
if ($this->hasClient($code)) {
$message = "Client {$code} is already defined";
if (null !== $this->serviceIds[$code] && null !== $serviceId) {
$message .= " by service \"{$this->serviceIds[$code]}\", cannot register service \"{$serviceId}\"";
$definedBy = $this->serviceIds[$code] ?? null;
if (null !== $definedBy && null !== $serviceId) {
$message .= " by service \"{$definedBy}\", cannot register service \"{$serviceId}\"";
}

throw new \UnexpectedValueException($message);
Expand All @@ -48,10 +77,16 @@ public function addClient(ClientInterface $client, ?string $serviceId = null): v
}

/**
* @return ClientInterface[]
* Instantiates every client of the locator.
*
* @return array<string, ClientInterface>
*/
public function getClients(): array
{
foreach (array_keys($this->locator?->getProvidedServices() ?? []) as $code) {
$this->getClient((string) $code);
}

return $this->clients;
}

Expand All @@ -60,15 +95,26 @@ public function getClients(): array
*/
public function getClient(string $code): ClientInterface
{
if (!$this->hasClient($code)) {
if (isset($this->clients[$code])) {
return $this->clients[$code];
}
if (!$this->locator?->has($code)) {
throw MissingClientException::create($code);
}

return $this->getClients()[$code];
$client = $this->locator->get($code);
if (!$client instanceof ClientInterface) {
throw new \UnexpectedValueException(\sprintf('The service of the SOAP client "%s" must implement "%s".', $code, ClientInterface::class));
}
if ($code !== $client->getCode()) {
throw new \UnexpectedValueException(\sprintf('The SOAP client "%s" is registered with the code "%s" but its getCode() method returns "%s": both must be the same.', $client::class, $code, $client->getCode()));
}

return $this->clients[$code] = $client;
}

public function hasClient(string $code): bool
{
return \array_key_exists($code, $this->getClients());
return isset($this->clients[$code]) || ($this->locator?->has($code) ?? false);
}
}
23 changes: 23 additions & 0 deletions tests/DependencyInjection/CleverAgeSoapProcessExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@

namespace CleverAge\SoapProcessBundle\Tests\DependencyInjection;

use CleverAge\SoapProcessBundle\Attribute\AsSoapClient;
use CleverAge\SoapProcessBundle\DependencyInjection\CleverAgeSoapProcessExtension;
use CleverAge\SoapProcessBundle\Registry\ClientRegistry;
use CleverAge\SoapProcessBundle\Task\RequestTask;
use CleverAge\SoapProcessBundle\Tests\Fixtures\AttributedClient;
use CleverAge\SoapProcessBundle\Transformer\RequestTransformer;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

#[CoversClass(CleverAgeSoapProcessExtension::class)]
#[UsesClass(RequestTransformer::class)]
#[CoversClass(AsSoapClient::class)]
#[UsesClass(ClientRegistry::class)]
class CleverAgeSoapProcessExtensionTest extends TestCase
{
public function testServices(): void
Expand All @@ -33,6 +41,21 @@ public function testServices(): void
(new CleverAgeSoapProcessExtension())->load([], $container);

self::assertSame(ClientRegistry::class, $container->getDefinition('cleverage_soap_process.registry.client')->getClass());
// Autowirable by its class name
self::assertSame('cleverage_soap_process.registry.client', (string) $container->getAlias(ClientRegistry::class));
}

public function testAsSoapClientAttributeRegistersTheCode(): void
{
$container = new ContainerBuilder();
(new CleverAgeSoapProcessExtension())->load([], $container);
$container->setDefinition('app.attributed', (new Definition(AttributedClient::class))->setAutoconfigured(true));

(new AttributeAutoconfigurationPass())->process($container);
(new ResolveInstanceofConditionalsPass())->process($container);
(new ResolveChildDefinitionsPass())->process($container);

self::assertSame([['code' => 'app_attributed']], $container->getDefinition('app.attributed')->getTag('cleverage.soap.client'));
}

public function testTaskIsRegistered(): void
Expand Down
Loading
Loading