How I Built a User Activity Log in Symfony That Non-Technical Users Could Actually Read

A few weeks ago, I found myself facing a problem that many developers eventually encounter.
My application already tracked user actions, but the activity log was only useful to developers.
The system recorded events like:
destination_deleted
offer_accepted
requirement_created
Technically, everything worked.
From a user's perspective, however, these entries were meaningless.
An administrator opening the activity page had no idea what actually happened, who performed the action, or why it mattered.
That's when I decided to redesign the activity logging system and focus on making it understandable for real users rather than just developers.
The Problem
Most developers start activity logging by storing action codes.
For example:
$activity->setAction('destination_deleted');
This is great for development because it's easy to filter and analyze.
The downside is that non-technical users don't think in action codes.
An administrator doesn't want to see:
destination_deleted
They want to see:
Sharon deleted the destination "Douala Port".
The difference seems small, but it completely changes the usability of the system.
My First Version
My first implementation stored only:
Action
User
Date
The database looked something like this:
| User | Action | Date |
|---|---|---|
| Sharon | destination_deleted | 2025-07-20 |
| John | requirement_created | 2025-07-20 |
It worked.
But every time someone wanted to investigate an issue, they had to ask a developer what the action code meant.
That wasn't sustainable.
The Solution
I decided to keep the technical action code while generating a human-readable message.
Instead of showing:
destination_deleted
The system now displays:
Sharon deleted the destination "Douala Port".
Instead of:
offer_accepted
Users see:
John accepted an offer for transport request #245.
This immediately made the activity log useful for support teams and administrators.
Designing the Activity Entity
The activity entity stores information such as:
class Activity
{
private ?User $user = null;
private ?string $action = null;
private ?string $message = null;
private ?\DateTimeImmutable $createdAt = null;
}
The key addition was the message field.
The action remains useful for filtering and reporting, while the message provides a clear explanation for humans.
Creating Activities
Whenever an important action occurs, an activity is created.
For example:
$activity = new Activity();
\(activity->setUser(\)user);
$activity->setAction('destination_deleted');
$activity->setMessage(
sprintf(
'%s deleted the destination "%s"',
$user->getFullName(),
$destination->getName()
)
);
\(entityManager->persist(\)activity);
Now every activity contains both technical and business-friendly information.
Improving the Admin Interface
The next challenge was displaying activities in EasyAdmin.
I wanted the newest activities to appear first.
Users typically care about what happened recently, not what happened six months ago.
Sorting by creation date solved this:
->setDefaultSort([
'createdAt' => 'DESC'
])
I also made activities read-only.
Logs should tell a story of what happened.
Allowing administrators to edit history would defeat their purpose.
Lessons Learned
This small feature taught me several important lessons.
1. Build for Humans
Developers understand action codes.
Users do not.
Always think about who will actually consume the information.
2. Keep Technical and Human Data Separate
The action code is useful for filtering.
The message is useful for reading.
Having both gives you flexibility.
3. Activity Logs Become Important Faster Than You Think
At first, logs seem like a minor feature.
Then a user reports a problem.
Suddenly everyone wants to know:
Who did it?
When did it happen?
What exactly changed?
A good activity log answers those questions immediately.
Final Thoughts
One of the most valuable lessons I've learned as a backend developer is that a feature isn't finished when it works.
A feature is finished when the people using it can understand it.
Transforming my activity log from a collection of technical action codes into a readable audit trail took only a small amount of extra work, but it dramatically improved the experience for administrators and support teams.
Sometimes the best improvements aren't new features.
They're making existing features easier for people to understand.