By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Here’s How You Can Build a FinTech Approval System With Symfony 7.4 | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Here’s How You Can Build a FinTech Approval System With Symfony 7.4 | HackerNoon
Computing

Here’s How You Can Build a FinTech Approval System With Symfony 7.4 | HackerNoon

News Room
Last updated: 2025/12/14 at 12:40 PM
News Room Published 14 December 2025
Share
Here’s How You Can Build a FinTech Approval System With Symfony 7.4 | HackerNoon
SHARE

The symfony/workflow component has long been one of the framework’s most powerful, yet underutilized, gems. It allows developers to decouple business process logic from entity state, transforming complex “spaghetti code” status checks into clean, visualizable directed graphs.

For years, however, the component had a strict limitation rooted in its implementation of Petri nets: Tokens were boolean. An object was either in a place (state), or it wasn’t. While you could be in multiple places simultaneously (parallel processing), you couldn’t be in the same place multiple times.

Symfony 7.4 changes the game with Weighted Transitions.

This new feature introduces multiplicity. You can now model scenarios where quantities matter: “collect 4 signatures,” “process 5 batch items,” or “wait for 3 subsystems to initialize.”

In this article, we will build a robust Multi-Signature Approval System for a FinTech application. We will explore how to configure weighted transitions, implement the entity logic, and verify the flow with rigorous testing — all using Symfony 7.4 and PHP 8.3.

The Concept (Petri Nets vs. State Machines)

Before writing code, it is crucial to understand why this feature exists.

The State Machine Limitation

A State Machine is linear. An elevator is either STOPPED, MOVINGUP, or MOVINGDOWN. It cannot be MOVINGUP twice. This is perfect for simpler statuses (e.g., Order::STATUSPAID).

The Workflow (Petri Net)

A Workflow allows an object to sit in multiple places at once. In a “New Employee Onboarding” process, an employee might simultaneously be in:

  • provisioning_laptop
  • creatingemailaccount

Both must be completed before they move to onboarded.

The Missing Piece: Multiplicity

Prior to Symfony 7.4, if you needed “3 Managers to approve an expense,” you couldn’t model this purely in the Workflow. You had to:

  1. Create a waitingforapproval place.
  2. Add a counter field to your entity ($approvalCount).
  3. Use a Guard Event listener to check if ($subject->getApprovalCount() >= 3) before allowing the transition.

With Weighted Transitions, the “counter” is now part of the workflow state itself. The workflow engine natively understands that the subject is in the approved state 3 times.

Project Setup

Let’s create a new Symfony project and install the necessary components.

composer create-project symfony/skeleton:"7.4.*" fintech-approval
cd fintech-approval
composer require symfony/workflow symfony/framework-bundle symfony/orm-pack symfony/maker-bundle

We will also need a database. For this example, we’ll use SQLite for simplicity, but the logic applies to MySQL/PostgreSQL exactly the same.

# .env
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

Configuration (The Core Logic)

This is where the magic happens. We will define a workflow called expense_approval.

The Scenario:

  1. An expense report is created (draft).
  2. It is submitted (review_required).
  3. The Weighted Step: The system distributes the request to 3 required approvers.
  4. Each approver grants approval individually.
  5. Once 3 approvals are collected, the expense moves to readyforpayment.

Create or update config/packages/workflow.yaml:

# config/packages/workflow.yaml
framework:
    workflows:
        expense_approval:
            type: workflow # MUST be 'workflow', not 'state_machine'
            audit_trail:
                enabled: true
            marking_store:
                type: method
                property: currentState # This must hold an array

            supports:
                - AppEntityExpenseReport

            initial_marking: draft

            places:
                - draft
                - review_pool
                - approved_pool
                - ready_for_payment
                - rejected

            transitions:
                submit:
                    from: draft
                    to:
                        - place: review_pool
                          weight: 3 # <--- OUTPUT WEIGHT

                approve:
                    from: review_pool
                    to: approved_pool
                    # Default weight is 1. One 'review_pool' token becomes one 'approved_pool' token.

                reject:
                    from: review_pool
                    to: rejected
                    # If rejected, we might want to clear all tokens, 
                    # but for simplicity, one rejection moves to rejected.

                finalize:
                    from:
                        - place: approved_pool
                          weight: 3 # <--- INPUT WEIGHT
                    to: ready_for_payment

Deconstructing the Config

  1. type: workflow: Weighted transitions rely on token buckets. This is not possible in a state machine.
  2. submitTransition: n – to: { place: reviewpool, weight: 3 } – When this fires, the reviewpoolplace receives 3 tokens. n – Think of this as creating 3 “tickets” that need to be punched.
  3. approveTransition: n –from: reviewpool, to: approvedpool. n – Standard 1-to-1 weight. n – Because we have 3 tokens in review_pool, we can fire this transition 3 times.
  4. finalizeTransition: n – from: { place: approvedpool, weight: 3 }- This transition is blocked until the approvedpoolcontains exactly (or at least) 3 tokens. n – Once the 3rd approval comes in, this path unlocks.

The Entity

We need an entity that supports this “Multi-State” marking store. The currentState property must be an array to hold the token counts (e.g., [‘reviewpool’ => 2, ‘approvedpool’ => 1]).

namespace AppEntity;

use AppRepositoryExpenseReportRepository;
use DoctrineORMMapping as ORM;

#[ORMEntity(repositoryClass: ExpenseReportRepository::class)]
class ExpenseReport
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn]
    private ?int $id = null;

    #[ORMColumn(length: 255)]
    private string $description;

    #[ORMColumn]
    private float $amount;

    /**
     * Stores the workflow state.
     * For Weighted Workflows, this stores the places and their quantities.
     * Example DB content: {"review_pool": 2, "approved_pool": 1}
     */
    #[ORMColumn(type: 'json')]
    private array $currentState = [];

    public function __construct(string $description, float $amount)
    {
        $this->description = $description;
        $this->amount = $amount;
        // Initial marking is handled by the workflow component, 
        // but initializing to empty array is good practice.
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function getAmount(): float
    {
        return $this->amount;
    }

    public function getCurrentState(): array
    {
        return $this->currentState;
    }

    public function setCurrentState(array $currentState): self
    {
        $this->currentState = $currentState;
        return $this;
    }
}

Run the migration:

php bin/console make:migration
php bin/console doctrine:migrations:migrate

The Service Layer

To interact with this workflow cleanly, we should create a service. This service will handle the logic of “who” is approving, though for this tutorial, we will focus on the workflow mechanics.

namespace AppService;

use AppEntityExpenseReport;
use SymfonyComponentWorkflowWorkflowInterface;
use SymfonyComponentWorkflowRegistry;

readonly class ExpenseManager
{
    public function __construct(
        private Registry $workflowRegistry,
    ) {}

    public function submit(ExpenseReport $expense): void
    {
        $workflow = $this->getWorkflow($expense);

        if ($workflow->can($expense, 'submit')) {
            $workflow->apply($expense, 'submit');
        } else {
            throw new LogicException('Cannot submit this expense report.');
        }
    }

    public function approve(ExpenseReport $expense): void
    {
        $workflow = $this->getWorkflow($expense);

        // In a real app, you would check "Is the current user one of the allowed approvers?" here.

        if ($workflow->can($expense, 'approve')) {
            $workflow->apply($expense, 'approve');

            // Check if we can auto-finalize (if 3 approvals are met)
            if ($workflow->can($expense, 'finalize')) {
                $workflow->apply($expense, 'finalize');
            }
        } else {
            throw new LogicException('Approval not needed or not allowed.');
        }
    }

    public function getStatus(ExpenseReport $expense): array
    {
        // returns something like ['review_pool' => 2, 'approved_pool' => 1]
        return $expense->getCurrentState();
    }

    private function getWorkflow(ExpenseReport $expense): WorkflowInterface
    {
        return $this->workflowRegistry->get($expense, 'expense_approval');
    }
}

The “Auto-Finalize” Logic

Notice the approve method. After applying approve, we immediately check can($expense, ‘finalize’).

  1. First Approval: n –reviewpool: 2 n –approvedpool: 1 n –finalize needs 3 approved_pool. can(‘finalize’) returns false.
  2. Second Approval: n –reviewpool: 1 n –approvedpool: 2 n –can(‘finalize’) returns false.
  3. Third Approval: n reviewpool: 0 n approvedpool: 3 n finalize needs 3. can(‘finalize’) returns true. n We apply finalize. n New State:readyforpayment: 1.

Visualizing the Workflow

Before testing, it is incredibly helpful to visualize the graph, especially with weights involved. Symfony provides a dumper command.

php bin/console workflow:dump expense_approval | dot -Tpng -o workflow.png

You need graphviz installed on your machine to use dot. If you don’t have it, you can paste the text output into an online Graphviz viewer.

The output will visually represent the arrows with labels like weight: 3, making it clear that the submit transition spawns multiple tokens.

Verification (Unit Testing)

We don’t just hope it works; we prove it. We will use a KernelTestCase to load the actual workflow configuration and test the transitions.

//tests/Workflow/ExpenseApprovalWorkflowTest.php
namespace AppTestsWorkflow;

use AppEntityExpenseReport;
use SymfonyBundleFrameworkBundleTestKernelTestCase;
use SymfonyComponentWorkflowWorkflowInterface;

class ExpenseApprovalWorkflowTest extends KernelTestCase
{
    private WorkflowInterface $workflow;

    protected function setUp(): void
    {
        self::bootKernel();
        $container = static::getContainer();
        $registry = $container->get('workflow.registry');

        // We create a dummy subject to get the workflow
        // In a real app, passing the class name to the registry is preferred if supported,
        // or fetching by name directly if you have a custom service alias.
        $subject = new ExpenseReport('Test', 100.0);
        $this->workflow = $registry->get($subject, 'expense_approval');
    }

    public function testWeightedApprovalFlow(): void
    {
        $expense = new ExpenseReport('MacBook Pro', 3000.00);

        // 1. Initial State
        $this->assertTrue($this->workflow->can($expense, 'submit'));
        $this->workflow->apply($expense, 'submit');

        // Verify tokens: Should be in review_pool 3 times
        $marking = $expense->getCurrentState();
        $this->assertArrayHasKey('review_pool', $marking);
        $this->assertEquals(3, $marking['review_pool'], 'Should have 3 pending reviews');

        // 2. First Approval
        $this->assertTrue($this->workflow->can($expense, 'approve'));
        $this->workflow->apply($expense, 'approve');

        $marking = $expense->getCurrentState();
        $this->assertEquals(2, $marking['review_pool']);
        $this->assertEquals(1, $marking['approved_pool']);

        // Check that we CANNOT finalize yet (need 3 approvals)
        $this->assertFalse($this->workflow->can($expense, 'finalize'), 'Should not finalize with only 1 approval');

        // 3. Second Approval
        $this->workflow->apply($expense, 'approve');
        $marking = $expense->getCurrentState();
        $this->assertEquals(1, $marking['review_pool']);
        $this->assertEquals(2, $marking['approved_pool']);

        // 4. Third Approval
        $this->workflow->apply($expense, 'approve');
        $marking = $expense->getCurrentState();

        // At this specific moment, review_pool is 0, approved_pool is 3
        $this->assertEquals(0, $marking['review_pool'] ?? 0);
        $this->assertEquals(3, $marking['approved_pool']);

        // 5. Finalize
        $this->assertTrue($this->workflow->can($expense, 'finalize'), 'Should be able to finalize now');
        $this->workflow->apply($expense, 'finalize');

        // 6. Verify End State
        $marking = $expense->getCurrentState();
        $this->assertArrayHasKey('ready_for_payment', $marking);
        $this->assertEquals(1, $marking['ready_for_payment']);
        // Previous tokens should be consumed
        $this->assertArrayNotHasKey('approved_pool', $marking);
    }
}

Run the test:

php bin/phpunit tests/Workflow/ExpenseApprovalWorkflowTest.php

Advanced Usage & Best Practices

Using Enums for Places (New in 7.4)

Symfony 7.4 also adds support for Backed Enums in workflows. Instead of hardcoding strings like ‘review_pool’, you should define an Enum.

namespace AppEnum;

enum ExpenseState: string
{
    case DRAFT = 'draft';
    case REVIEW_POOL = 'review_pool';
    case APPROVED_POOL = 'approved_pool';
    case READY = 'ready_for_payment';
    case REJECTED = 'rejected';
}

You can then update your workflow.yaml (though YAML still uses strings, your PHP code can use ExpenseState::REVIEW_POOL->value).

Guard Events with Weights

You might want to prevent the same person from approving 3 times. Weighted transitions allow the transition approve to be called 3 times, but the workflow engine doesn’t inherently know who called it.

To solve this, use a Guard Listener.

#[AsEventListener('workflow.expense_approval.guard.approve')]
public function preventDoubleApproval(GuardEvent $event): void
{
    /** @var ExpenseReport $expense */
    $expense = $event->getSubject();
    $user = $this->security->getUser();

    // Imagine the entity has a list of who already approved
    if ($expense->hasApproved($user)) {
        $event->setBlocked(true, 'You have already approved this expense.');
    }
}

Handling Rejection (Token Cleanup)

One challenge with weighted tokens is cleanup. If we have 2 approvals and the 3rd person calls reject, what happens to the 2 tokens sitting in approved_pool?

In a standard workflow, moving to rejected might leave those approved_pool tokens straggling (creating a zombie state where the report is both Rejected and Partially Approved).

The reject transition should ideally consume all tokens. However, dynamic consumption isn’t supported purely in YAML (you can’t say “consume ALL”).

Using an Event Subscriber on workflow.entered.rejected, you can manually reset the marking store.

public function onRejected(Event $event): void
{
    $expense = $event->getSubject();
    // Force reset state to ONLY rejected
    $expense->setCurrentState(['rejected' => 1]);
}

Conclusion

Symfony 7.4’s Weighted Workflow Transitions bridge the gap between simple state management and complex Petri net logic. By allowing multiple instances of a state (tokens) to exist simultaneously, we can now model voting systems, manufacturing assembly lines, and batch processing logic directly in the configuration, drastically reducing the amount of custom PHP boilerplate required.

Key Takeaways:

  1. Multiplicity: Places can hold multiple tokens, allowing for parallel accumulation of state.
  2. Configuration: Use weight in your to (output) and from (input) definitions to control token flow.
  3. Storage: Ensure your Marking Store property is an array (e.g., a JSON column) to track token counts per place.
  4. Verification: Always utilize the workflow:dump command and write KernelTestCase tests to prove your graph logic before deployment.

This feature solidifies Symfony’s Workflow component as the premier PHP solution for state management, allowing you to delete fragile “counter” properties and rely on a mathematically sound architecture.

Let’s Be in Touch

Adopting these advanced patterns can significantly simplify your domain logic, but the transition isn’t always obvious. If you are looking to modernize your Symfony stack, need a second pair of eyes on your architecture, or just want to geek out over the latest Petri net implementations, I’d love to hear from you.

Let’s be in touch — connect with me on LinkedIn (https://www.linkedin.com/in/matthew-mochalkin/) to continue the conversation on modern PHP architecture.

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Premier League Soccer: Stream Nottingham Forest vs. Tottenham Live From Anywhere Premier League Soccer: Stream Nottingham Forest vs. Tottenham Live From Anywhere
Next Article 4 Things You Can Actually Store In Apple Wallet – BGR 4 Things You Can Actually Store In Apple Wallet – BGR
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

How Roomba invented the home robot — and lost the future
How Roomba invented the home robot — and lost the future
News
Today&apos;s NYT Connections Hints, Answers for Dec. 16 #919
Today's NYT Connections Hints, Answers for Dec. 16 #919
News
Assessing Validity Threats in Controlled Software Engineering Experiments | HackerNoon
Assessing Validity Threats in Controlled Software Engineering Experiments | HackerNoon
Computing
The Best Gaming Chairs We’ve Tested for 2026
The Best Gaming Chairs We’ve Tested for 2026
News

You Might also Like

Assessing Validity Threats in Controlled Software Engineering Experiments | HackerNoon
Computing

Assessing Validity Threats in Controlled Software Engineering Experiments | HackerNoon

5 Min Read
HDMI Licensing Administrator, Inc. Showcases Advanced HDMI® Gaming Technologies at CES 2026 | HackerNoon
Computing

HDMI Licensing Administrator, Inc. Showcases Advanced HDMI® Gaming Technologies at CES 2026 | HackerNoon

3 Min Read
Study Finds Software Testers Often Misjudge Which Techniques Work Best | HackerNoon
Computing

Study Finds Software Testers Often Misjudge Which Techniques Work Best | HackerNoon

11 Min Read
Here’s Why Your UEBA Isn’t Working (and How to Fix It) | HackerNoon
Computing

Here’s Why Your UEBA Isn’t Working (and How to Fix It) | HackerNoon

14 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?