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
21 changes: 14 additions & 7 deletions documentation/user/getting-started/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,25 +218,32 @@ declare(strict_types=1);
namespace App\Workflow;

use App\Workflow\Activity\GreetingActivities;
use Gplanchat\Durable\Activity\ActivityStub;
use Gplanchat\Durable\Attribute\Activities;
use Gplanchat\Durable\Attribute\AsWorkflow;
use Gplanchat\Durable\Attribute\AsWorkflowMethod;
use Gplanchat\Durable\WorkflowEnvironment;

#[AsWorkflow(name: 'greet')]
final class GreetWorkflow
{
public function __construct(private readonly WorkflowEnvironment $environment) {}

/** @param ActivityStub<GreetingActivities> $greeting */
#[AsWorkflowMethod]
public function run(string $name): string
{
$activities = $this->environment->activityStub(GreetingActivities::class);

return $this->environment->await($activities->greet($name));
public function run(
string $name,
#[Activities(GreetingActivities::class)]
ActivityStub $greeting,
WorkflowEnvironment $env,
): string {
return $env->await($greeting->greet($name));
}
}
```

`$name` vient de l'entrée avec laquelle le workflow démarre. `$greeting` et `$env`, non : Durable
les fournit, comme Symfony fournit ses services à un contrôleur. Voir
[Les arguments que fournit Durable](../workflows/#arguments-durable-supplies).

### 4. Le déclencher depuis un contrôleur ou un service {#4--le-déclencher-depuis-un-contrôleur-ou-un-service}

```php
Expand Down
21 changes: 14 additions & 7 deletions documentation/user/getting-started/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,25 +218,32 @@ declare(strict_types=1);
namespace App\Workflow;

use App\Workflow\Activity\GreetingActivities;
use Gplanchat\Durable\Activity\ActivityStub;
use Gplanchat\Durable\Attribute\Activities;
use Gplanchat\Durable\Attribute\AsWorkflow;
use Gplanchat\Durable\Attribute\AsWorkflowMethod;
use Gplanchat\Durable\WorkflowEnvironment;

#[AsWorkflow(name: 'greet')]
final class GreetWorkflow
{
public function __construct(private readonly WorkflowEnvironment $environment) {}

/** @param ActivityStub<GreetingActivities> $greeting */
#[AsWorkflowMethod]
public function run(string $name): string
{
$activities = $this->environment->activityStub(GreetingActivities::class);

return $this->environment->await($activities->greet($name));
public function run(
string $name,
#[Activities(GreetingActivities::class)]
ActivityStub $greeting,
WorkflowEnvironment $env,
): string {
return $env->await($greeting->greet($name));
}
}
```

`$name` comes from the input the workflow is started with. `$greeting` and `$env` do not: Durable
supplies them, the way Symfony supplies a controller's services. See
[Arguments Durable supplies](../workflows/#arguments-durable-supplies).

### 4. Dispatch from a controller or service {#4--dispatch-from-a-controller-or-service}

```php
Expand Down
41 changes: 40 additions & 1 deletion documentation/user/workflows/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,45 @@ Un message enregistré après le déclenchement de l'échéance n'est jamais app
cette échéance a tranchée ; il reste disponible pour l'attente suivante, et son gestionnaire
s'exécute à ce moment-là. Voir **DUR032** et **DUR035**.

### Les arguments que fournit Durable {#arguments-durable-supplies}

La méthode du workflow peut recevoir ses stubs d'activités et son environnement en arguments, au
lieu de les construire dans un constructeur :

```php
/** @param ActivityStub<OrderActivities> $orders */
#[AsWorkflowMethod]
public function run(
string $orderId,
#[Activities(OrderActivities::class)]
ActivityStub $orders,
WorkflowEnvironment $env,
): mixed {
return $env->await($orders->charge($orderId));
}
```

- Un paramètre typé **`WorkflowEnvironment`** reçoit l'environnement.
- Un paramètre typé **`ActivityStub`** et marqué **`#[Activities(Contrat::class)]`** reçoit
`$env->activityStub(Contrat::class)`. PHP n'a pas de génériques à l'exécution : c'est l'attribut
qui nomme le contrat.
- Tout autre paramètre est une **entrée**, lue par son nom, comme avant. Les paramètres fournis ne
sont jamais passés par l'appelant : ni par le code qui démarre le workflow, ni par un parent qui
l'appelle comme enfant, ni par une opération Nexus.
- Le docblock **`@param ActivityStub<Contrat>`** sert à PHPStan. Avec
[`gplanchat/durable-phpstan`](../packages/), un docblock qui nomme un autre contrat que
l'attribut est une erreur, et son absence aussi (`durable.activities.missingGeneric`, que l'on
peut ignorer), puisque PHPStan ne peut pas vérifier les appels sans lui.
- Un stub qui a besoin d'**`ActivityOptions`** garde `$env->activityStub($contrat, $options)` : les
arguments d'un attribut ne savent pas construire une `Duration`.
- Les erreurs surviennent dès l'**enregistrement** du workflow (compilation du conteneur, avec le
bundle) : un `ActivityStub` sans `#[Activities]`, un `#[Activities]` sur un autre type, un
contrat introuvable, ou un contrat qui ne déclare aucun `#[AsActivityMethod]`.

La forme par constructeur reste valable. C'est celle qu'il faut quand la classe implémente une
interface de contrat comme `OrderWorkflowContract` plus haut : PHP n'autorise pas l'implémentation à
ajouter des paramètres obligatoires à `run()`.

### `ActivityOptions` sur le stub

Pour appliquer **réessais**, **délais**, **file de tâches** et métadonnées de planification voisines à tous les appels passant par un stub donné, passez des **`ActivityOptions`** en second argument d'**`activityStub()`** :
Expand Down Expand Up @@ -292,7 +331,7 @@ ce qu'un workflow peut faire, et rien de ce que le moteur garde pour lui.
| `some($count, ...$awaitables)` | Se résout quand `$count` membres ont **réussi**, indexés par position de déclaration. Les autres sont annulés. |
| `timer($duration, $summary = '')` | Un awaitable qui se résout à l'échéance de la durée. Se compose comme n'importe quel autre. |
| `sleep($duration, $summary = '')` | Attend, et fait l'attente pour vous. Dit ce qu'il fait. |
| `activityStub($contract, $options = null)` | Un proxy typé sur un contrat d'activité. Construisez-le dans le constructeur ; tous ses appels portent `$options`. |
| `activityStub($contract, $options = null)` | Un proxy typé sur un contrat d'activité. Construisez-le dans le constructeur, ou déclarez-le en [argument `#[Activities]`](#arguments-durable-supplies) s'il n'a pas besoin d'options ; tous ses appels portent `$options`. |
| `childWorkflowStub($class, $options = null)` | Le même, pour un workflow enfant : résolu depuis la classe de l'enfant, et ses appels se composent comme les autres. |
| `onSignal($name, $handler)` | Enregistre un gestionnaire de signal. Le gestionnaire mute l'état du workflow et `await()` l'observe ; il n'y a pas d'attente séparée. Le nom prend une énumération adossée, donc une faute de frappe est une erreur de type et non une attente qui ne se résout jamais. |
| `onUpdate($name, $handler)` | Le même pour une mise à jour, dont la valeur de retour du gestionnaire est la réponse rendue à l'appelant. |
Expand Down
41 changes: 40 additions & 1 deletion documentation/user/workflows/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,45 @@ reached, **including** when the awaited signal is delivered after the deadline e
recorded after the deadline fired is never applied to the wait that deadline settled; it stays
available to the next wait, and its handler runs then. See **DUR032** and **DUR035**.

### Arguments Durable supplies

The workflow method can take its activity stubs and its environment as arguments instead of
building them in a constructor:

```php
/** @param ActivityStub<OrderActivities> $orders */
#[AsWorkflowMethod]
public function run(
string $orderId,
#[Activities(OrderActivities::class)]
ActivityStub $orders,
WorkflowEnvironment $env,
): mixed {
return $env->await($orders->charge($orderId));
}
```

- A parameter typed **`WorkflowEnvironment`** receives the environment.
- A parameter typed **`ActivityStub`** and marked **`#[Activities(Contract::class)]`** receives
`$env->activityStub(Contract::class)`. PHP has no runtime generics, so the attribute is what names
the contract.
- Every other parameter is **input**, matched by name, as before. A caller never passes the
supplied ones: not the code that starts the workflow, not a parent calling it as a child, not a
Nexus operation.
- The **`@param ActivityStub<Contract>`** docblock is for PHPStan. With
[`gplanchat/durable-phpstan`](../packages/), a docblock that names another contract than the
attribute is an error, and so is a missing one (`durable.activities.missingGeneric`, which a
project can ignore), since PHPStan cannot check the calls without it.
- A stub that needs **`ActivityOptions`** keeps `$env->activityStub($contract, $options)`: attribute
arguments cannot build a `Duration`.
- Mistakes fail when the workflow is **registered** (container compilation, with the bundle): an
`ActivityStub` without `#[Activities]`, `#[Activities]` on another type, a contract that does not
exist, or one that declares no `#[AsActivityMethod]`.

The constructor form keeps working. It is the one to use when the class implements a contract
interface such as `OrderWorkflowContract` above: PHP does not let the implementation add required
parameters to `run()`.

### ActivityOptions on the stub

To apply **retries**, **timeouts**, **task queue**, and related scheduling metadata to every call made through a given stub, pass **`ActivityOptions`** as the second argument to **`activityStub()`**:
Expand Down Expand Up @@ -286,7 +325,7 @@ everything a workflow can do, and nothing the engine keeps for itself.
| `some($count, ...$awaitables)` | Settles when `$count` members have **succeeded**, indexed by declaration position. The rest are cancelled. |
| `timer($duration, $summary = '')` | An awaitable that settles when the duration elapses. Composes like any other. |
| `sleep($duration, $summary = '')` | Waits, and awaits for you. Says what it does. |
| `activityStub($contract, $options = null)` | A typed proxy over an activity contract. Build it in the constructor; every call it makes carries `$options`. |
| `activityStub($contract, $options = null)` | A typed proxy over an activity contract. Build it in the constructor, or declare it as an [`#[Activities]` argument](#arguments-durable-supplies) when it needs no options; every call it makes carries `$options`. |
| `childWorkflowStub($class, $options = null)` | The same, for a child workflow: resolved from the child's class, and its calls compose like any other. |
| `onSignal($name, $handler)` | Registers a signal handler. The handler mutates workflow state and `await()` observes it; there is no separate wait. The name takes a backed enum, so a typo is a type error rather than a wait that never settles. |
| `onUpdate($name, $handler)` | The same for an update, whose handler's return value is the caller's response. |
Expand Down
29 changes: 29 additions & 0 deletions src/Durable/Attribute/Activities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Gplanchat\Durable\Attribute;

/**
* Names the activity contract of an `ActivityStub` parameter of a workflow method.
*
* ```php
* #[AsWorkflowMethod]
* public function run(string $name, #[Activities(GreetingActivities::class)] ActivityStub $greeting, WorkflowEnvironment $env): string
* ```
*
* The loader reads it once, when the workflow is registered, and hands the method
* `$env->activityStub(GreetingActivities::class)`. PHP has no runtime generics, so the attribute is
* what carries the contract; the `@param ActivityStub<GreetingActivities>` docblock is only there
* for PHPStan.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final class Activities
{
/**
* @param class-string $contract
*/
public function __construct(
public readonly string $contract,
) {}
}
8 changes: 6 additions & 2 deletions src/Durable/Stub/StubArguments.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,22 @@ private function __construct() {}
* @param array<int|string, mixed> $arguments as `__call` received them: the positional ones
* under indices, the named ones under their name
*
* @param list<\ReflectionParameter>|null $parameters the parameters a caller passes, when some of
* `$method`'s are supplied by something else; all of
* them by default
*
* @return array<string, mixed> the named payload, one contract parameter per key
*
* @throws \BadMethodCallException if a named argument matches no parameter, if a required
* parameter is not supplied, or if a parameter is served both
* positionally and by name
*/
public static function toPayload(\ReflectionFunctionAbstract $method, array $arguments): array
public static function toPayload(\ReflectionFunctionAbstract $method, array $arguments, ?array $parameters = null): array
{
$payload = [];
$known = [];

foreach ($method->getParameters() as $i => $param) {
foreach ($parameters ?? $method->getParameters() as $i => $param) {
$name = $param->getName();
$known[$name] = true;

Expand Down
3 changes: 2 additions & 1 deletion src/Durable/Workflow/ChildWorkflowStub.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public function __call(string $name, array $arguments): \Gplanchat\Durable\Await
*/
private function argumentsToInput(array $arguments): array
{
return StubArguments::toPayload($this->workflowMethod, $arguments);
// A parent passes the input only: the child's injected parameters are the loader's to supply.
return StubArguments::toPayload($this->workflowMethod, $arguments, WorkflowDefinitionLoader::inputParameters($this->workflowMethod));
}
}
Loading
Loading