Skip to content
Open
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
10 changes: 8 additions & 2 deletions documentation/user/getting-started/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,21 @@ when@dev:

### Marquer les workflows

Toute classe portant `#[AsWorkflow]` dans votre espace de noms de workflows est enregistrée automatiquement dès que vous marquez le dossier :
Rien à écrire. Une classe portant `#[AsWorkflow]` est enregistrée dès qu'elle est un service — ce
qu'avec l'`autoconfigure: true` par défaut d'une application Symfony elle est déjà.

Les versions précédentes demandaient de marquer le dossier à la main :

```yaml
# config/services.yaml
# config/services.yaml — désormais inutile
App\Workflow\:
resource: '../src/Workflow/'
tags: [durable.workflow]
```

La balise fonctionne toujours : une application qui l'écrit continue de marcher, elle fait
simplement double emploi.

### Déclarer les implémentations d'activité

Les classes d'implémentation d'activité sont des services Symfony ordinaires (l'autowiring s'applique). Si vous posez `#[AsActivityHandler]` sur la classe, le bundle les ramasse tout seul dès que le service est marqué.
Expand Down
9 changes: 7 additions & 2 deletions documentation/user/getting-started/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,20 @@ when@dev:

### Tag workflows

Any class annotated with `#[AsWorkflow]` in your workflow namespace is auto-registered when you tag the folder:
Nothing to write. A class carrying `#[AsWorkflow]` is registered as soon as it is a service, which
with the default `autoconfigure: true` of a Symfony application it already is.

Earlier versions required tagging the folder by hand:

```yaml
# config/services.yaml
# config/services.yaml — no longer needed
App\Workflow\:
resource: '../src/Workflow/'
tags: [durable.workflow]
```

The tag still works, so an application that writes it keeps working; it is simply redundant.

### Register activity implementations

Activity implementation classes are registered as normal Symfony services (autowiring applies). If you use `#[AsActivityHandler]` on the class, the bundle picks them up automatically when the service is tagged.
Expand Down
6 changes: 4 additions & 2 deletions documentation/user/packages/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ composer require gplanchat/durable-bundle

Ce qu'il fait, et que vous écririez autrement à la main :

- **L'autoconfiguration.** Les classes portant `#[AsWorkflow]` et `#[AsActivity]` s'enregistrent seules ;
vous ne les listez pas dans un fichier de conteneur.
- **L'autoconfiguration.** Les classes portant `#[AsWorkflow]` ou `#[AsActivityHandler]`
s'enregistrent seules ; vous ne les listez pas dans un fichier de conteneur, et vous ne les
balisez pas non plus. `#[AsActivity]` est un attribut de nommage posé sur le contrat, pas
d'enregistrement.
- **Le câblage Messenger.** Reprises de workflow et envois d'activité sont routés vers les transports
que vous nommez dans `durable.yaml`, si bien qu'un workflow qui se suspend reprend par vos files
existantes.
Expand Down
5 changes: 3 additions & 2 deletions documentation/user/packages/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ composer require gplanchat/durable-bundle

What it does that you would otherwise write by hand:

- **Autoconfiguration.** Classes carrying `#[AsWorkflow]` and `#[AsActivity]` are registered on their
own; you do not list them in a container file.
- **Autoconfiguration.** Classes carrying `#[AsWorkflow]` or `#[AsActivityHandler]` register
themselves; you do not list them in a container file, and you do not tag them either.
`#[AsActivity]` is a naming attribute on the contract, not a registration one.
- **Messenger wiring.** Workflow resumes and activity dispatches are routed to the transports you
name in `durable.yaml`, so a workflow that suspends resumes through your existing queues.
- **One console command.** `durable:execution:diagnose <executionId>` prints what the engine holds
Expand Down
11 changes: 11 additions & 0 deletions src/DurableBundle/DurableBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Gplanchat\Durable\Bundle;

use Gplanchat\Durable\Attribute\AsActivityHandler;
use Gplanchat\Durable\Attribute\AsWorkflow;
use Gplanchat\Durable\Attribute\AsNexusServiceHandler;
use Gplanchat\Durable\Attribute\FulfilsNexusOperation;
use Gplanchat\Durable\Bundle\DependencyInjection\Compiler\ActivityHandlerPass;
Expand All @@ -28,6 +29,16 @@ static function (ChildDefinition $definition, AsActivityHandler $attribute, \Ref
},
);

// Le quatrième, et il manquait. `WorkflowDefinitionLoader` lit déjà `#[AsWorkflow]` pour
// nommer le type ; c'est ici que la classe devient trouvable par le registre, sans balise
// écrite à la main dans le `services.yaml` de l'application.
$container->registerAttributeForAutoconfiguration(
AsWorkflow::class,
static function (ChildDefinition $definition, AsWorkflow $_attribute, \Reflector $_reflector): void {
$definition->addTag('durable.workflow');
},
);

$container->registerAttributeForAutoconfiguration(
AsNexusServiceHandler::class,
static function (ChildDefinition $definition, AsNexusServiceHandler $attribute, \Reflector $_reflector): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace unit\Gplanchat\DurableBundle\DependencyInjection;

use Gplanchat\Durable\Bundle\DependencyInjection\DurableExtension;
use Gplanchat\Durable\Bundle\DurableBundle;
use Gplanchat\Durable\WorkflowRegistry;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use unit\DurableBundle\Fixtures\PasUnWorkflow;
use unit\DurableBundle\Fixtures\WorkflowAvecEnvironnement;
use unit\DurableBundle\Fixtures\WorkflowSansRien;

/**
* Trois attributs sur quatre s'autoconfigurent. Le quatrième — celui qui déclare un workflow —
* demandait une balise écrite à la main, par répertoire, dans le `services.yaml` de l'application.
*
* L'issue #255 annonce un piège : un workflow est instancié par réflexion par
* `WorkflowDefinitionLoader`, jamais par le conteneur, et son constructeur reçoit un
* `WorkflowEnvironment` qui n'est pas un service. Baliser sans plus ferait donc, dit-elle, échouer
* la compilation sur une classe que le conteneur ne construira jamais.
*
* Ces cas l'éprouvent au lieu de le supposer : ils compilent réellement le conteneur.
*/
final class AsWorkflowAutoconfigurationTest extends TestCase
{
public function testUnWorkflowAttributeEstEnregistreSansBaliseEcriteALaMain(): void
{
$container = $this->compileWith([WorkflowSansRien::class]);

self::assertContains(
WorkflowSansRien::class,
$this->registeredClasses($container),
"l'attribut doit suffire, comme il suffit déjà pour les trois autres",
);
}

/**
* Le cœur de la question. Si le piège de #255 mordait, cet appel lèverait à la compilation.
*/
public function testUnWorkflowQuiRecoitLEnvironnementCompileQuandMeme(): void
{
$container = $this->compileWith([WorkflowAvecEnvironnement::class]);

self::assertContains(WorkflowAvecEnvironnement::class, $this->registeredClasses($container));
}

public function testUneClasseSansAttributNeRejointPasLeRegistre(): void
{
$container = $this->compileWith([PasUnWorkflow::class]);

self::assertNotContains(PasUnWorkflow::class, $this->registeredClasses($container));
}

/**
* @param list<class-string> $classes
*/
private function compileWith(array $classes): ContainerBuilder
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', false);
(new DurableExtension())->load([[]], $container);

// Ce que FrameworkExtension pose et que ce conteneur synthétique n'a pas : le bus par
// défaut, référencé par le dispatcher de reprise.
$container->register('messenger.default_bus', \stdClass::class)->setPublic(true);

foreach ($classes as $class) {
$container->register($class, $class)
->setAutoconfigured(true)
->setAutowired(true)
->setPublic(false)
;
}

(new DurableBundle())->build($container);
$container->compile();

return $container;
}

/**
* @return list<string>
*/
private function registeredClasses(ContainerBuilder $container): array
{
$registered = [];
foreach ($container->getDefinition(WorkflowRegistry::class)->getMethodCalls() as [$method, $arguments]) {
if ('registerClass' === $method) {
$registered[] = (string) $arguments[0];
}
}

return $registered;
}
}
16 changes: 16 additions & 0 deletions tests/unit/DurableBundle/Fixtures/PasUnWorkflow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace unit\DurableBundle\Fixtures;

/**
* Ne porte pas l'attribut : ne doit pas rejoindre le registre.
*/
final class PasUnWorkflow
{
public function run(): string
{
return 'non';
}
}
26 changes: 26 additions & 0 deletions tests/unit/DurableBundle/Fixtures/WorkflowAvecEnvironnement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace unit\DurableBundle\Fixtures;

use Gplanchat\Durable\Attribute\AsWorkflow;
use Gplanchat\Durable\Attribute\AsWorkflowMethod;
use Gplanchat\Durable\WorkflowEnvironment;

/**
* La forme que le guide de démarrage enseigne : un constructeur qui reçoit l'environnement, lequel
* n'est **pas** un service du conteneur. C'est cette forme que l'issue #255 annonce comme piégeuse
* dès qu'on autoconfigure l'attribut.
*/
#[AsWorkflow('AvecEnvironnement')]
final class WorkflowAvecEnvironnement
{
public function __construct(private readonly WorkflowEnvironment $environment) {}

#[AsWorkflowMethod]
public function run(string $quoi): string
{
return $quoi;
}
}
21 changes: 21 additions & 0 deletions tests/unit/DurableBundle/Fixtures/WorkflowSansRien.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace unit\DurableBundle\Fixtures;

use Gplanchat\Durable\Attribute\AsWorkflow;
use Gplanchat\Durable\Attribute\AsWorkflowMethod;

/**
* Un workflow sans dépendance : rien à autowirer, donc rien qui puisse piéger.
*/
#[AsWorkflow('SansRien')]
final class WorkflowSansRien
{
#[AsWorkflowMethod]
public function run(): string
{
return 'ok';
}
}
Loading