baskin/history-bundle
baskin/history-bundle adds revision tracking to your PHP models, recording each change so you can audit edits, review prior versions, and see how data evolved over time. Lightweight and straightforward to integrate into existing projects.
Confirm compatibility before install. Check the package’s composer.json constraints first:
composer show baskin/history-bundle --all
This bundle is old and has no dependents/stars, so validate that it supports your Symfony/Twig/Doctrine versions before adding it to a project.
Install Gedmo Loggable as the real dependency. The bundle is only a Twig presentation layer. You still need DoctrineExtensions Loggable:
composer require gedmo/doctrine-extensions
# or, commonly:
composer require stof/doctrine-extensions-bundle
Register the bundle using the style for your Symfony version:
// Symfony 4+/5+ config/bundles.php
return [
Baskin\HistoryBundle\BaskinHistoryBundle::class => ['all' => true],
];
// Older Symfony app/AppKernel.php
$bundles = [
new Baskin\HistoryBundle\BaskinHistoryBundle(),
];
Configure the Loggable listener:
# config/packages/stof_doctrine_extensions.yaml
stof_doctrine_extensions:
orm:
default:
loggable: true
Annotate the entities you want to audit:
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @Gedmo\Loggable
*/
class Product
{
/**
* @Gedmo\Versioned
*/
private $name;
/**
* @Gedmo\Versioned
*/
private $price;
}
Create the log table:
bin/console doctrine:schema:update --force
# or generate a migration:
bin/console doctrine:migrations:diff
Find the bundle’s Twig API first. Look in vendor/baskin/history-bundle for the Twig extension class and its service definition. Confirm the exact function/filter names before writing templates. The usage pattern is typically:
{% for log in history(product) %}
<tr>
<td>{{ log.loggedAt|date('Y-m-d H:i') }}</td>
<td>{{ log.username ?: 'system' }}</td>
<td>{{ log.action }}</td>
<td>
{% for field, value in log.data %}
<strong>{{ field }}</strong>: {{ value }}
{% endfor %}
</td>
</tr>
{% endfor %}
If the bundle does not expose what you need, bypass it and use the Gedmo repository directly.
First useful use case: add a “History” tab to an admin detail page. Pass the entity from the controller, render the log entries in reverse chronological order, and show changed fields only.
Fetch the entity normally, then let Twig render its log entries:
public function show(Product $product): Response
{
return $this->render('product/show.html.twig', [
'product' => $product,
]);
}
Use the bundle’s Twig function in the template, or fall back to the Gedmo repository:
use Gedmo\Loggable\Entity\LogEntry;
$logs = $em->getRepository(LogEntry::class)->getLogEntries($product);
Keep history rendering in one partial so every entity gets consistent output:
{# templates/admin/_history.html.twig #}
{% if history(entity) is not empty %}
<table class="table table-sm">
<thead>
<tr>
<th>When</th>
<th>Who</th>
<th>Action</th>
<th>Changes</th>
</tr>
</thead>
<tbody>
{% for log in history(entity)|reverse %}
<tr>
<td>{{ log.loggedAt|date('Y-m-d H:i:s') }}</td>
<td>{{ log.username ?: 'system' }}</td>
<td>{{ log.action }}</td>
<td>
{% for field, value in log.data %}
<div><strong>{{ field }}</strong>: {{ value }}</div>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No history yet.</p>
{% endif %}
Gedmo stores the full versioned payload per log entry, so a raw loop repeats unchanged fields. Build a diff against the previous version:
$previous = [];
$changesByLog = [];
foreach ($logs as $log) {
$changes = [];
foreach ($log->getData() as $field => $value) {
$old = $previous[$field] ?? null;
if ($old !== $value) {
$changes[$field] = ['old' => $old, 'new' => $value];
}
}
$changesByLog[] = $changes;
$previous = $log->getData();
}
Pass changesByLog to Twig. Be careful with ordering: if you display newest first, diff against the next older entry instead.
A common admin feature is “revert to this version.” Read the log entry’s data, call matching setters, and flush:
$log = $logs[$version];
foreach ($log->getData() as $field => $value) {
$setter = 'set' . ucfirst($field);
if (method_exists($product, $setter)) {
$product->$setter($value);
}
}
$em->flush();
Treat this carefully for relations, dates, and value objects. Logged relation data may be IDs or serialized values, not hydrated entities.
Do not load every log entry for entities with years of edits. Query the log repository directly:
$qb = $em->createQueryBuilder()
->select('l')
->from(LogEntry::class, 'l')
->where('l.objectClass = :class')
->andWhere('l.objectId = :id')
->setParameter('class', get_class($product))
->setParameter('id', $product->getId())
->orderBy('l.loggedAt', 'DESC');
Then wire the query into Pagerfanta, KnpPaginator, or your existing pagination layer.
@Versioned if you do not want their values stored in ext_log_entries.stof/doctrine-extensions-bundle to configure the Loggable listener rather than wiring Gedmo services manually.LogEntryRepository::getLogEntries(). That is usually less work than fighting an old bundle.history.html.twig partial plus a small presenter/service for diffing and label translation.$labels = [
'create' => 'Created',
'update' => 'Updated',
'remove' => 'Deleted',
];
vendor/baskin/history-bundle. Function names and argument order may differ from the examples above.@Versioned properties. Changes to unannotated fields are invisible in history. This is usually correct, but it surprises developers during debugging.ext_log_entries.UPDATE ... SET ... via DQL bypasses history.objectId then points to a deleted record. Render deleted-entity history defensively instead of assuming the entity still exists.data. Expect strings/IDs rather than live objects. Normalize values before comparing or displaying them.getLogEntries() returns entries in a defined order, but confirm it. If you reverse for display, compute diffs against the correct adjacent version.ext_log_entries directly, verify the Doctrine listener is active, check that the entity manager is flushing the entity, and clear the Symfony cache.bin/console debug:twig
# older Symfony:
bin/console twig:debug
LogEntry entity if you need extra metadata.How can I help you explore Laravel packages today?