ActivityLogBundle - Extended doctrine loggable (StofDoctrineExtensionsBundle)
ActivityLogBundle uses Loggable extension from StofDoctrineExtensionsBundle and DoctrineExtensions
This bundle extend Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry with below fields:
- parentId - store depedency to "main entity"
- parentClass - store "main entity" type
- oldData - data that were changed
- name - entry name (to show in activity log)
- user - associations mapping with user who changed data
Bundle contain extended listener (LoggableListener) to process above fields.
Also available formatter to preprocessing activity log before show in view (html).
Pretty simple with Composer, run:
composer require madmis/activity-log-bundle
Then enable the bundle in the kernel:
public function registerBundles()
{
$bundles = [
// ...
new ActivityLogBundle\ActivityLogBundle(),
// ...
];
...
}
Configure bundle:
# app/config/config.yml
doctrine:
dbal:
#...
orm:
#...
resolve_target_entities:
Symfony\Component\Security\Core\User\UserInterface: AppBundle\Entity\User
mappings:
gedmo_loggable:
type: annotation
prefix: Gedmo\Loggable\Entity
dir: "%kernel.root_dir%/../src/AppBundle/Entity/"
alias: GedmoLoggable
is_bundle: false
stof_doctrine_extensions:
class:
loggable: ActivityLogBundle\Listener\LoggableListener
orm:
default:
loggable: true
Create entity and make it loggable:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use ActivityLogBundle\Entity\Interfaces\StringableInterface;
/**
* @package AppBundle\Entity
* @ORM\Entity(repositoryClass="ProjectRepository")
* @ORM\Table
* @Gedmo\Loggable(logEntryClass="ActivityLogBundle\Entity\LogEntry")
*/
class Project implements StringableInterface
{
/**
* @var int
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
* @ORM\Column(type="string", length=128)
* @Gedmo\Versioned
*/
private $name;
/**
* @var string
* @ORM\Column(type="string", length=16)
* @Gedmo\Versioned
*/
private $key;
//...
StringableInterface required to save LogEntry::name.
Then run command to update database schema:
php bin/console doctrine:schema:update --force
Formatter class: ActivityLogBundle\Service\ActivityLog\ActivityLogFormatter Formatter service: activity_log.formatter
required: LoggerInterface as dependency
By default entity without custom formatter class formatted by ActivityLogBundle\Service\ActivityLog\EntityFormatter\UniversalFormatter
But you can implement custom formatter for each entity.
To register a custom formatter, add a service tag with the following (required) properties:
- name: 'activity_log.formatter'
- entity: Class name of the entity that should be formatted by the registered formatter
Example:
services:
app.formatter.project:
class: AppBundle\Service\ActivityFormatter\Project
tags:
- { name: activity_log.formatter, entity: 'Project'}
As example formatter for AppBundle\Entity\Project entity:
namespace AppBundle\Service\ActivityFormatter;
class Project extends AbstractFormatter implements FormatterInterface
{
/**
* @param LogEntryInterface $log
* @return array
*/
public function format(LogEntryInterface $log)
{
$result = $log->toArray();
if ($log->isCreate()) {
$result['message'] = sprintf('The <b>Project <span class="font-green-jungle">"%s"</span></b> was created.', $log->getName());
} else if ($log->isRemove()) {
$result['message'] = sprintf('The <b>Project <span class="font-red-flamingo">"%s"</span></b> was removed.', $log->getName());
} else if ($log->isUpdate()) {
$result['message'] = '<dl><dt>The <b>Project <span class="font-yellow-gold">"%s"</span></b> was updated.</dt>%s</dl>';
$data = $log->getData();
$oldData = $log->getOldData();
$text = '';
foreach ($data as $field => $value) {
$value = $this->normalizeValue($field, $value);
if (array_key_exists($field, $oldData)) {
$oldValue = $this->normalizeValue($field, $oldData[$field]);
$subText = sprintf('from "<b>%s</b>" to "<b>%s</b>".', $oldValue, $value);
} else {
$subText = sprintf('to "<b>%s</b>".', $value);
}
$text .= sprintf('<dd>Property "<b>%s</b>" was changed: %s</dd>', $field, $subText);
}
$result['message'] = sprintf($result['message'], $log->getName(), $text);
} else {
$result['message'] = "Undefined action: {$log->getAction()}.";
}
return $result;
}
}
If entity has association with other entity it can be resolved by AbstractFormatter::normalizeValue. This method call method from the entity formatter class, which named as appropriate property.
For example, Project entity has association mapping ManyToOne to Type entity. To get Type name we can add method type to Project formatter:
namespace AppBundle\Service\ActivityFormatter;
class Project extends AbstractFormatter implements FormatterInterface
{
//...
/**
* @param array $value
* @return string
*/
protected function type(array $value)
{
if (isset($value['id'])) {
/** @var Type $entity */
$entity = $this->entityManager->getRepository('AppBundle:Type')
->find($value['id']);
if ($entity) {
return $entity->getName();
}
}
return '';
}
As result we have formatted response to show in view.
$em = $this->getDoctrine()->getManager();
// get log entries for entity
$entries = $em
->getRepository('AppBundle:LogEntry')
->getLogEntriesQueryBuilder($entity)
->getQuery()
->getResult();
// format log entries to show in the view
$entries = $this
->get('activity_log.formatter')
->format($entries);
For $entity
should be configured Entity formatter.