In this Step 4, we want to show that a serious ERP/CRM doesn’t just have a simple “status” field, but a real approval engine:
- requests that go through multiple steps (e.g., Manager → Director → Finance)
- a status that evolves over time (
PENDING → APPROVED → REJECTED → ESCALATED) - assignment of the task to different people (
assigned_to) - possible escalations if something stays stuck for too long
To avoid building a full workflow engine (too big for a “showcase” page), we focus on one very clear piece:
The “approve current step” logic:
given anApprovalRequestinPENDINGstatus, when the user approves:
- if there are more steps → move to the next step and reassign
- if this is the last step → mark the request as
APPROVED.
This micro-function is perfect to compare frameworks:
it’s small, but it contains state, business logic, and persistence.
2. Shared scenario and selected piece to implement
We use the same simplified entity in all frameworks:
Entity: ApprovalRequest
Minimal fields:
idtarget_type(e.g.,"PROJECT","DISCOUNT")target_id(project id, client id, etc.)current_step(int)status(string:PENDING,APPROVED,REJECTED,ESCALATED)assigned_to(string: email or user id)
Function we want to demonstrate:
approveStep(requestId, totalSteps, approvedByUser)
Behavior:
- Load the
ApprovalRequest - If
status≠PENDING→ do nothing - If
current_step<totalSteps:- increment
current_step - reassign
assigned_to(e.g., to the next role/user)
- increment
- If
current_step==totalSteps:- set
status = APPROVED - optionally keep or clear
assigned_to(depending on the policy)
- set
We don’t handle REJECT or ESCALATE here: we simply mention them as possible states, but the example stays focused on the approval transition.
3. How each framework handles “Approve Step”
Below we only show the core approveStep logic, so the page stays readable.
3.1 PHP + MySQL (procedural)
function approve_step(mysqli $db, int $requestId, int $totalSteps, string $approvedBy): void
{
$sql = "SELECT * FROM approval_requests WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->bind_param("i", $requestId);
$stmt->execute();
$result = $stmt->get_result();
$request = $result->fetch_assoc();
if (!$request || $request['status'] !== 'PENDING') {
return; // nothing to do
}
$currentStep = (int)$request['current_step'];
if ($currentStep < $totalSteps) {
$nextStep = $currentStep + 1;
// in a real system we would resolve the next approver based on role/step
$nextAssignee = 'director@company.com';
$updateSql = "
UPDATE approval_requests
SET current_step = ?, assigned_to = ?
WHERE id = ?
";
$up = $db->prepare($updateSql);
$up->bind_param("isi", $nextStep, $nextAssignee, $requestId);
$up->execute();
} else {
$updateSql = "
UPDATE approval_requests
SET status = 'APPROVED'
WHERE id = ?
";
$up = $db->prepare($updateSql);
$up->bind_param("i", $requestId);
$up->execute();
}
}
Here you clearly see the “bare metal” version: manual queries, full control.
3.2 Laravel (PHP – Eloquent ORM)
// App/Models/ApprovalRequest.php
class ApprovalRequest extends Model
{
protected $fillable = [
'target_type',
'target_id',
'current_step',
'status',
'assigned_to',
];
}
// App/Services/ApprovalWorkflowService.php
class ApprovalWorkflowService
{
public function approveStep(int $requestId, int $totalSteps, string $approvedBy): void
{
$request = ApprovalRequest::find($requestId);
if (! $request || $request->status !== 'PENDING') {
return;
}
if ($request->current_step < $totalSteps) {
$request->current_step++;
// in a real case: resolve the next approver from a roles table
$request->assigned_to = 'director@company.com';
} else {
$request->status = 'APPROVED';
}
$request->save();
}
}
Same logic, but much more readable: database access is abstracted behind Eloquent.
3.3 Django (Python – ORM)
# models.py
from django.db import models
class ApprovalRequest(models.Model):
target_type = models.CharField(max_length=50)
target_id = models.IntegerField()
current_step = models.IntegerField(default=1)
status = models.CharField(max_length=20, default='PENDING')
assigned_to = models.EmailField()
# services.py
from .models import ApprovalRequest
def approve_step(request_id: int, total_steps: int, approved_by: str) -> None:
try:
req = ApprovalRequest.objects.get(pk=request_id)
except ApprovalRequest.DoesNotExist:
return
if req.status != 'PENDING':
return
if req.current_step < total_steps:
req.current_step += 1
# in a real application, resolve the next approver by role/team
req.assigned_to = 'director@company.com'
else:
req.status = 'APPROVED'
req.save()
The structure is very similar to Laravel: a simple model plus a service function.
3.4 Node.js + Express + Sequelize
// models/ApprovalRequest.js
const { DataTypes } = require("sequelize");
const sequelize = require("../db");
const ApprovalRequest = sequelize.define("ApprovalRequest", {
target_type: DataTypes.STRING,
target_id: DataTypes.INTEGER,
current_step: { type: DataTypes.INTEGER, defaultValue: 1 },
status: { type: DataTypes.STRING, defaultValue: "PENDING" },
assigned_to: DataTypes.STRING,
});
module.exports = ApprovalRequest;
// workflow.js
const ApprovalRequest = require("./models/ApprovalRequest");
async function approveStep(requestId, totalSteps, approvedBy) {
const req = await ApprovalRequest.findByPk(requestId);
if (!req || req.status !== "PENDING") return;
if (req.current_step < totalSteps) {
req.current_step += 1;
req.assigned_to = "director@company.com";
} else {
req.status = "APPROVED";
}
await req.save();
}
module.exports = { approveStep };
Here the pattern is asynchronous, but the application logic stays identical.
3.5 Spring Boot (Java – JPA + Service)
// Entity
@Entity
public class ApprovalRequest {
@Id @GeneratedValue
private Long id;
private String targetType;
private Long targetId;
private int currentStep;
private String status; // PENDING, APPROVED, ...
private String assignedTo; // email or username
// getters/setters
}
// Repository
public interface ApprovalRequestRepository extends JpaRepository<ApprovalRequest, Long> { }
// Service
@Service
public class ApprovalWorkflowService {
@Autowired
private ApprovalRequestRepository repo;
public void approveStep(Long requestId, int totalSteps, String approvedBy) {
ApprovalRequest req = repo.findById(requestId)
.orElseThrow(() -> new IllegalArgumentException("Not found"));
if (!"PENDING".equals(req.getStatus())) {
return;
}
if (req.getCurrentStep() < totalSteps) {
req.setCurrentStep(req.getCurrentStep() + 1);
req.setAssignedTo("director@company.com");
} else {
req.setStatus("APPROVED");
}
repo.save(req);
}
}
Here you see strong typing and the classic repository + service pattern.
3.6 ASP.NET Core (C# – EF Core)
public class ApprovalRequest
{
public int Id { get; set; }
public string TargetType { get; set; } = "";
public int TargetId { get; set; }
public int CurrentStep { get; set; } = 1;
public string Status { get; set; } = "PENDING";
public string AssignedTo { get; set; } = "";
}
public class AppDbContext : DbContext
{
public DbSet<ApprovalRequest> ApprovalRequests { get; set; }
}
public class ApprovalWorkflowService
{
private readonly AppDbContext _context;
public ApprovalWorkflowService(AppDbContext context)
{
_context = context;
}
public void ApproveStep(int requestId, int totalSteps, string approvedBy)
{
var req = _context.ApprovalRequests.Find(requestId);
if (req == null || req.Status != "PENDING")
return;
if (req.CurrentStep < totalSteps)
{
req.CurrentStep += 1;
req.AssignedTo = "director@company.com";
}
else
{
req.Status = "APPROVED";
}
_context.SaveChanges();
}
}
Very similar to Spring in architecture, but in the Microsoft ecosystem.
3.7 Drupal (PHP – Entity API)
/**
* Approve step for a custom approval_request entity.
*/
function mymodule_approve_step($request_id, $total_steps, $approved_by) {
$storage = \Drupal::entityTypeManager()->getStorage('approval_request');
/** @var \Drupal\Core\Entity\EntityInterface $approval */
$approval = $storage->load($request_id);
if (!$approval) {
return;
}
$status = $approval->get('field_status')->value;
if ($status !== 'PENDING') {
return;
}
$current_step = (int) $approval->get('field_current_step')->value;
if ($current_step < $total_steps) {
$approval->set('field_current_step', $current_step + 1);
$approval->set('field_assigned_to', 'director@company.com');
}
else {
$approval->set('field_status', 'APPROVED');
}
$approval->save();
}
Here everything goes through Drupal’s Entity API and field system.
4. Pros and cons table (for this single “Approve Step” function)
| Framework | Main pros | Main cons |
|---|---|---|
| PHP + MySQL | Maximum control, no magic, runs on almost any hosting | Lots of boilerplate, risk of SQL errors, no built-in validation/abstraction |
| Laravel | Expressive syntax, powerful Eloquent, easy to add events, logs, policies | Requires knowledge of the Laravel ecosystem, some “magic” under the hood |
| Django | Mature ORM, very readable code, easy integration with Django REST | Less flexible for very custom SQL, learning curve for Python + Django |
| Node + Sequelize | Great for JSON APIs, async-ready, easy to integrate with SPA frontends | Tooling less opinionated, must pay attention to async error handling and typing |
| Spring Boot | Strong typing, fits enterprise architectures, highly testable services | More verbose, heavier initial setup |
| ASP.NET Core | High performance, excellent in Microsoft environments, powerful EF Core | Higher lock-in to MS stack, less immediate for PHP/Python developers |
| Drupal | Workflow integrated with content and permissions, ideal for editorial portals | Overkill if you only need business logic, APIs feel heavier for pure PHP devs |
5. Final disclaimer (ArchPilot style)
Disclaimer
The contents of this page, including code examples and architectural descriptions, are provided for informational and educational purposes only. They do not constitute professional advice (technical, legal, tax, or otherwise), nor any guarantee of accuracy, completeness, or suitability for production use.
Any use of the information provided is entirely at the user’s own risk and responsibility. Always perform your own technical, legal, and security reviews before adopting any solution in real-world environments.
All trademarks mentioned are the property of their respective owners. Any references to third-party technologies, frameworks, or products are for descriptive purposes only and do not imply any affiliation, endorsement, or sponsorship.
