Skip to content

Instantly share code, notes, and snippets.

@soyuka
Created May 15, 2018 13:24
Show Gist options
  • Save soyuka/7c75933a6ae3d64940bb1d1f0d9fa9da to your computer and use it in GitHub Desktop.
Save soyuka/7c75933a6ae3d64940bb1d1f0d9fa9da to your computer and use it in GitHub Desktop.
Workflow bridge for api platform

Just load the WorkflowPass compiler pass and the services.yaml in your project.

After that, when a class supports workflow it'll have two routes (assuming entity dummy):

api_dummies_state_item                                        PATCH    ANY      ANY    /api/dummies/{id}/state.{_format}

PATCH with data (must be state key) {"state": "state_name"} to change a given resource's state to state_name.

api_dummies_state_get_item                                    GET      ANY      ANY    /api/dummies/{id}/state.{_format}

GET receives available states for the given resource.

<?php
declare(strict_types=1);
namespace ApiPlatform\Workflow\PathResolver;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
final class OperationPathResolver implements OperationPathResolverInterface
{
private $decorated;
public function __construct(OperationPathResolverInterface $decorated)
{
$this->decorated = $decorated;
}
public function resolveOperationPath(string $resourceShortName, array $operation, $operationType/*, string $operationName = null*/): string
{
$path = $this->decorated->resolveOperationPath($resourceShortName, $operation, $operationType, null);
if (!isset($operation['_path_suffix'])) {
return $path;
}
return str_replace('{id}', '{id}'.$operation['_path_suffix'], $path);
}
}
services:
_defaults:
autowire: true
public: false
ApiPlatform\Workflow\Metadata\Resource\Factory\WorkflowOperationResourceMetadataFactory:
decorates: 'api_platform.metadata.resource.metadata_factory'
arguments:
- []
- '@ApiPlatform\Workflow\Metadata\Resource\Factory\WorkflowOperationResourceMetadataFactory.inner'
ApiPlatform\Workflow\PathResolver\OperationPathResolver:
decorates: 'api_platform.operation_path_resolver.generator'
arguments:
- '@ApiPlatform\Workflow\PathResolver\OperationPathResolver.inner'
ApiPlatform\Workflow\EventListener\WorkflowStateListener:
# ReadListener in api platform is 4
# DeserializeListener in api platform is 2
tags:
- { name: kernel.event_listener, event: kernel.request, priority: 1 }
ApiPlatform\Workflow\EventListener\WorkflowEnabledTransitionsListener:
tags:
- { name: kernel.event_listener, event: kernel.view, priority: 21 }
<?php
declare(strict_types=1);
namespace ApiPlatform\Workflow\EventListener;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Workflow\Registry;
final class WorkflowEnabledTransitionsListener
{
private $workflows;
private $serializer;
public function __construct(SerializerInterface $serializer, Registry $workflows)
{
$this->workflows = $workflows;
$this->serializer = $serializer;
}
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
$attributes = RequestAttributesExtractor::extractAttributes($request);
if (!$request->isMethod(Request::METHOD_GET)
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
|| !isset($attributes['item_operation_name'])
|| 'state_get' !== $attributes['item_operation_name']
) {
return;
}
$class = $request->attributes->get('data');
$workflow = $this->workflows->get($class);
$event->setResponse(new Response($this->serializer->serialize($workflow->getEnabledTransitions($class), 'json')));
}
}
<?php
declare(strict_types=1);
namespace ApiPlatform\Workflow\Metadata\Resource\Factory;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
final class WorkflowOperationResourceMetadataFactory implements ResourceMetadataFactoryInterface
{
private $supportsWorkflow;
private $decorated;
public function __construct(array $supportsWorkflow = [], ResourceMetadataFactoryInterface $decorated)
{
$this->supportsWorkflow = $supportsWorkflow;
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*/
public function create(string $resourceClass): ResourceMetadata
{
$resourceMetadata = $this->decorated->create($resourceClass);
if (!in_array($resourceClass, $this->supportsWorkflow, true)) {
return $resourceMetadata;
}
$operations = $resourceMetadata->getItemOperations();
$operations['state'] = [
'method' => 'PATCH',
'_path_suffix' => '/state',
];
$operations['state_get'] = [
'method' => 'GET',
'_path_suffix' => '/state',
];
return $resourceMetadata->withItemOperations($operations);
}
}
<?php
declare(strict_types=1);
namespace ApiPlatform\Workflow\DependencyInjection\Compiler;
use ApiPlatform\Workflow\Metadata\Resource\Factory\WorkflowOperationResourceMetadataFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class WorkflowPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$registry = $container->getDefinition('workflow.registry');
$factory = $container->getDefinition(WorkflowOperationResourceMetadataFactory::class);
$arguments = [];
foreach ($registry->getMethodCalls() as $methodCall) {
$supportsStrategy = $methodCall[1][1];
$arguments[] = $supportsStrategy->getArguments()[0];
}
$factory->setArgument(0, $arguments);
}
}
<?php
declare(strict_types=1);
namespace ApiPlatform\Workflow\EventListener;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Workflow\Registry;
final class WorkflowStateListener
{
private $workflows;
public function __construct(Registry $workflows)
{
$this->workflows = $workflows;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$attributes = RequestAttributesExtractor::extractAttributes($request);
if (!$request->isMethod(Request::METHOD_PATCH)
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
|| !isset($attributes['item_operation_name'])
|| 'state' !== $attributes['item_operation_name']
) {
return;
}
$requestContent = json_decode($request->getContent());
if (!isset($requestContent->state) || !($state = $requestContent->state)) {
throw new BadRequestHttpException('State is required.');
}
$class = $request->attributes->get('data');
$workflow = $this->workflows->get($class);
// @TODO replace by violation of some sort (validator?)
// if (!$workflow->can($class, $state)) {
// throw new UnprocessableEntityHttpException("Can not process to state '$state'");
// }
$workflow->apply($class, $state);
}
}
@varun-sharma-astra
Copy link

Hi @soyuka !
Thank you for the gist.

I'm currently using this in a SF6.4 + PHP8.3 project.
Can we use this concept to create a POST request?

My requirement is to create a global bulk transition API where we will pass a JSON object in request and then we will iterate over them and change the state of each object.

@soyuka
Copy link
Author

soyuka commented Oct 15, 2024

sure but I'd suggest using a processor instead of the listener from above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment