Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
use Doctrine\ORM\Mapping\MappedSuperclass;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use phpDocumentor\Reflection\Types\ContextFactory;
use PhpParser\Parser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPUnit\Framework\TestCase;
use PhpParser\Parser;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface as PsrClockInterface;
use Psr\Container\ContainerInterface as PsrContainerInterface;
Expand All @@ -33,11 +31,11 @@
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
use Symfony\Bundle\FullStack;
use Symfony\Bundle\MercureBundle\MercureBundle;
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\PackageInterface;
use Symfony\Component\AssetMapper\AssetMapper;
use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
use Symfony\Component\AssetMapper\Compressor\CompressorInterface;
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\PackageInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
Expand All @@ -50,9 +48,9 @@
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -185,7 +183,6 @@
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
use Symfony\Component\Scheduler\Messenger\Serializer\Normalizer\SchedulerTriggerNormalizer;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
Expand Down Expand Up @@ -224,20 +221,20 @@
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\Attribute\ExtendsValidationFor;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass as ValidatorAttributeMetadataPass;
use Symfony\Component\Validator\GroupProviderInterface;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\ObjectInitializerInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Webhook\Controller\WebhookController;
use Symfony\Component\WebLink\HttpHeaderParser;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Symfony\Component\Webhook\Controller\WebhookController;
use Symfony\Component\Workflow;
use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Workflow\Attribute\AsWorkflow;
use Symfony\Component\Workflow\Configuration\AttributeReader;
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\Cache\CacheInterface;
Expand All @@ -249,6 +246,8 @@
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use phpDocumentor\Reflection\Types\ContextFactory;

/**
* Process the configuration and prepare the dependency injection container with
Expand Down Expand Up @@ -829,6 +828,14 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu
);
}

$attributeReader = new AttributeReader();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is broken if the workflow component is not installed.

$container->registerAttributeForAutoconfiguration(AsWorkflow::class, static function (ChildDefinition $definition, AsWorkflow $attribute, \ReflectionClass $reflection) use ($attributeReader): void {
$configuration = $attributeReader->extractConfiguration($attribute, $reflection);
$definition->addTag('.workflow.attribute', [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A way to solve my objection in https://github.com/symfony/symfony/pull/61935/files#r2399400826 would be to define the attribute as AsWorkflowDefinition instead, and register .workflow.attribute as a resource tag instead, similar to how #61563 was implemented.

'configuration' => $configuration,
]);
});

$container->registerForAutoconfiguration(CompilerPassInterface::class)
->addTag('container.excluded', ['source' => 'because it\'s a compiler pass']);
$container->registerForAutoconfiguration(Constraint::class)
Expand Down Expand Up @@ -1087,190 +1094,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $

$loader->load('workflow.php');

$registryDefinition = $container->getDefinition('workflow.registry');

foreach ($config['workflows'] as $name => $workflow) {
$type = $workflow['type'];
$workflowId = \sprintf('%s.%s', $type, $name);

// Process Metadata (workflow + places (transition is done in the "create transition" block))
$metadataStoreDefinition = new Definition(Workflow\Metadata\InMemoryMetadataStore::class, [[], [], null]);
if ($workflow['metadata']) {
$metadataStoreDefinition->replaceArgument(0, $workflow['metadata']);
}
$placesMetadata = [];
foreach ($workflow['places'] as $place) {
if ($place['metadata']) {
$placesMetadata[$place['name']] = $place['metadata'];
}
}
if ($placesMetadata) {
$metadataStoreDefinition->replaceArgument(1, $placesMetadata);
}

// Create transitions
$transitions = [];
$guardsConfiguration = [];
$transitionsMetadataDefinition = new Definition(\SplObjectStorage::class);
// Global transition counter per workflow
$transitionCounter = 0;
foreach ($workflow['transitions'] as $transition) {
foreach (['from', 'to'] as $direction) {
foreach ($transition[$direction] as $k => $arc) {
$transition[$direction][$k] = new Definition(Arc::class, [$arc['place'], $arc['weight'] ?? 1]);
}
}
if ('workflow' === $type) {
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
$container->register($transitionId, Workflow\Transition::class)
->setArguments([$transition['name'], $transition['from'], $transition['to']]);
$transitions[] = new Reference($transitionId);
if (isset($transition['guard'])) {
$eventName = \sprintf('workflow.%s.guard.%s', $name, $transition['name']);
$guardsConfiguration[$eventName][] = new Definition(
Workflow\EventListener\GuardExpression::class,
[new Reference($transitionId), $transition['guard']]
);
}
if ($transition['metadata']) {
$transitionsMetadataDefinition->addMethodCall('offsetSet', [
new Reference($transitionId),
$transition['metadata'],
]);
}
} elseif ('state_machine' === $type) {
foreach ($transition['from'] as $from) {
foreach ($transition['to'] as $to) {
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
$container->register($transitionId, Workflow\Transition::class)
->setArguments([$transition['name'], [$from], [$to]]);
$transitions[] = new Reference($transitionId);
if (isset($transition['guard'])) {
$eventName = \sprintf('workflow.%s.guard.%s', $name, $transition['name']);
$guardsConfiguration[$eventName][] = new Definition(
Workflow\EventListener\GuardExpression::class,
[new Reference($transitionId), $transition['guard']]
);
}
if ($transition['metadata']) {
$transitionsMetadataDefinition->addMethodCall('offsetSet', [
new Reference($transitionId),
$transition['metadata'],
]);
}
}
}
}
}
$metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition);
$metadataStoreId = \sprintf('%s.metadata_store', $workflowId);
$container->setDefinition($metadataStoreId, $metadataStoreDefinition);

// Create places
$places = array_column($workflow['places'], 'name');
$initialMarking = $workflow['initial_marking'] ?? [];

// Create a Definition
$definitionDefinition = new Definition(Workflow\Definition::class);
$definitionDefinition->addArgument($places);
$definitionDefinition->addArgument($transitions);
$definitionDefinition->addArgument($initialMarking);
$definitionDefinition->addArgument(new Reference($metadataStoreId));
$definitionDefinitionId = \sprintf('%s.definition', $workflowId);

// Create MarkingStore
$markingStoreDefinition = null;
if (isset($workflow['marking_store']['type']) || isset($workflow['marking_store']['property'])) {
$markingStoreDefinition = new ChildDefinition('workflow.marking_store.method');
$markingStoreDefinition->setArguments([
'state_machine' === $type, // single state
$workflow['marking_store']['property'] ?? 'marking',
]);
} elseif (isset($workflow['marking_store']['service'])) {
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
}

// Validation
$workflow['definition_validators'][] = match ($workflow['type']) {
'state_machine' => Workflow\Validator\StateMachineValidator::class,
'workflow' => Workflow\Validator\WorkflowValidator::class,
default => throw new \LogicException(\sprintf('Invalid workflow type "%s".', $workflow['type'])),
};

// Create Workflow
$workflowDefinition = new ChildDefinition(\sprintf('%s.abstract', $type));
$workflowDefinition->replaceArgument(0, new Reference($definitionDefinitionId));
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
$workflowDefinition->replaceArgument(3, $name);
$workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']);

$workflowDefinition->addTag('workflow', [
'name' => $name,
'metadata' => $workflow['metadata'],
'definition_validators' => $workflow['definition_validators'],
'definition_id' => $definitionDefinitionId,
]);
if ('workflow' === $type) {
$workflowDefinition->addTag('workflow.workflow', ['name' => $name]);
} elseif ('state_machine' === $type) {
$workflowDefinition->addTag('workflow.state_machine', ['name' => $name]);
}

// Store to container
$container->setDefinition($workflowId, $workflowDefinition);
$container->setDefinition($definitionDefinitionId, $definitionDefinition);
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type, $name);

// Add workflow to Registry
if ($workflow['supports']) {
foreach ($workflow['supports'] as $supportedClassName) {
$strategyDefinition = new Definition(Workflow\SupportStrategy\InstanceOfSupportStrategy::class, [$supportedClassName]);
$registryDefinition->addMethodCall('addWorkflow', [new Reference($workflowId), $strategyDefinition]);
}
} elseif (isset($workflow['support_strategy'])) {
$registryDefinition->addMethodCall('addWorkflow', [new Reference($workflowId), new Reference($workflow['support_strategy'])]);
}

// Enable the AuditTrail
if ($workflow['audit_trail']['enabled']) {
$listener = new Definition(Workflow\EventListener\AuditTrailListener::class);
$listener->addTag('monolog.logger', ['channel' => 'workflow']);
$listener->addTag('kernel.event_listener', ['event' => \sprintf('workflow.%s.leave', $name), 'method' => 'onLeave']);
$listener->addTag('kernel.event_listener', ['event' => \sprintf('workflow.%s.transition', $name), 'method' => 'onTransition']);
$listener->addTag('kernel.event_listener', ['event' => \sprintf('workflow.%s.enter', $name), 'method' => 'onEnter']);
$listener->addArgument(new Reference('logger'));
$container->setDefinition(\sprintf('.%s.listener.audit_trail', $workflowId), $listener);
}

// Add Guard Listener
if ($guardsConfiguration) {
if (!class_exists(ExpressionLanguage::class)) {
throw new LogicException('Cannot guard workflows as the ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
}

if (!class_exists(AuthenticationEvents::class)) {
throw new LogicException('Cannot guard workflows as the Security component is not installed. Try running "composer require symfony/security-core".');
}

$guard = new Definition(Workflow\EventListener\GuardListener::class);

$guard->setArguments([
$guardsConfiguration,
new Reference('workflow.security.expression_language'),
new Reference('security.token_storage'),
new Reference('security.authorization_checker'),
new Reference('security.authentication.trust_resolver'),
new Reference('security.role_hierarchy'),
new Reference('validator', ContainerInterface::NULL_ON_INVALID_REFERENCE),
]);
foreach ($guardsConfiguration as $eventName => $config) {
$guard->addTag('kernel.event_listener', ['event' => $eventName, 'method' => 'onTransition']);
}

$container->setDefinition(\sprintf('.%s.listener.guard', $workflowId), $guard);
$container->setParameter('workflow.has_guard_listeners', true);
}
}
$container->setParameter('.workflow.config', $config);
}

private function registerDebugConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
Expand Down
2 changes: 2 additions & 0 deletions 2 src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
use Symfony\Component\VarExporter\Internal\Registry;
use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass;
use Symfony\Component\Workflow\DependencyInjection\WorkflowGuardListenerPass;
use Symfony\Component\Workflow\DependencyInjection\WorkflowServiceCreatorPass;
use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass;

// Help opcache.preload discover always-needed symbols
Expand Down Expand Up @@ -181,6 +182,7 @@ public function build(ContainerBuilder $container): void
$this->addCompilerPassIfExists($container, FormPass::class);
$this->addCompilerPassIfExists($container, WorkflowGuardListenerPass::class);
$this->addCompilerPassIfExists($container, WorkflowValidatorPass::class);
$this->addCompilerPassIfExists($container, WorkflowServiceCreatorPass::class);
$container->addCompilerPass(new ResettableServicePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new RegisterLocaleAwareServicesPass());
$container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32);
Expand Down
48 changes: 48 additions & 0 deletions 48 src/Symfony/Component/Workflow/Attribute/AsWorkflow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow\Attribute;

use Symfony\Component\Workflow\WorkflowType;

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsWorkflow
{
public function __construct(
public string $name,
public string|array $places = [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any array should have phpdoc describing the expected type of this array (otherwise, we risk having to cover array<int|string, mixed> with BC as it won't be clear what works accidentally even if it was not intentional)

public WorkflowType $type = WorkflowType::StateMachine,
public array $supports = [],
public array $markingStore = [],
public bool $auditTrail = true,
) {
if (\is_string($this->places)) {
if (!enum_exists($this->places)) {
throw new \InvalidArgumentException(\sprintf('The "places" attribute of the "%s" workflow must be an array or a valid enum name, "%s" given.', self::class, $this->places));
}
if (!is_a($this->places, \BackedEnum::class, true)) {
throw new \InvalidArgumentException(\sprintf('The "places" attribute of the "%s" workflow must be a backed enum', self::class));
}
$this->places = array_map(fn (\UnitEnum $case) => $case->value, $this->places::cases());
}
foreach ($this->places as $k => $place) {
if (is_string($place)) {
$this->places[$k] = [
'name' => $place,
'metadata' => [],
];
}
}
}
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.