diff --git a/README.md b/README.md index 6543cf6..a52dc68 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ -# Pager bundle +# Audit bundle -This paginator is different in the following ways: +This bundle creates an audit log for all doctrine ORM database related changes: -- it has only one general class with around 300 lines of commented code. All the rest of the source code is -specific to symfony2 framework, twig helpers and templates. -- it allows to create custom pagination filters - search, select.. and modify the database query based on the specific -use cases. -- it also handles sorting in the traditional way. -- it is very small and may be reused in other frameworks with the modifications needed. -- it can only paginate Doctrine2 ORM QueryBuilder. Nothing else will be supported to maintain this library small and -backward compatible. For your own customizations just fork or copy the source code. -- there may be only one pagination per request, because url query parameters are constant. +- inserts and updates including their diffs and relation field diffs. +- many to many relation changes, association and dissociation actions. +- if there is an user in token storage, it will link him to the log. + +Basically you can track any change from these log entries if they were +managed through standard **ORM** operations. + +**NOTE:** audit cannot track DQL or direct SQL updates or delete statement executions. ## Demo @@ -19,8 +18,7 @@ and run: make -Visit **http://localhost:8000** to see the paginated fake projects with custom -filters and sorters. +Visit **http://localhost:8000/audit** to see the log actions. The demo application source is available in **example** directory and it is a basic symfony application. @@ -29,349 +27,22 @@ symfony application. First, install it with composer: - composer require data-dog/pager-bundle + composer require data-dog/audit-bundle Then, add it in your **AppKernel** bundles. ## Usage -The general usage example in your controller: - -``` php -getDoctrine()->getManager()->getRepository("AppBundle:Project") - ->createQueryBuilder('p') - ->addSelect('l') - ->innerJoin('p.language', 'l'); - - $projects = new Pagination($qb, $request); - return compact('projects'); - } -} -``` - -All you need is to construct Pagination with [doctrine query builder](http://doctrine-orm.readthedocs.org/en/latest/reference/query-builder.html) and -the request. The **Pagination** object acts like an array, so you can pass it to the view and iterate over paginated items. - -The view: - -``` twig - - - - - - - - - - - - {% for project in projects %} - - - - - {% if project.isOverDeadline %} - - {% else %} - - {% endif %} - - - {% endfor %} - -
#{{ sorter_link(projects, "p.code", "Code") }}{{ sorter_link(projects, "p.name", "Name") }}{{ sorter_link(projects, "p.hoursSpent", "Hours Spent") }}{{ sorter_link(projects, "l.code", "Language") }}
{{ project.id }}{{ project.code }}{{ project.name }}{{ project.hoursSpent }}{{ project.hoursSpent }}{{ project.language.code }}
- - -``` - -There are **twig** helper functions used: - -- **sorter_link** - which uses the twig template to generate a link with the sorting order class and such. -- **pagination** - which creates a pagination html code to navigate pages. - -These templates may be modified in standard symfony ways, see the configuration section. - -### Filters - -In order to filter paginated results in different kinds of ways, you may extend the code. -In the controller, provide some pagination options. - -``` php -andWhere($qb->expr()->like('p.name', "'%{$val}%'")); - } else { - // this allows us to safely ignore empty values - // otherwise if $qb is not changed, it would add where the string is empty statement. - $qb->andWhere('1 = 1'); - } - break; - case 'p.hoursSpent': - switch ($val) { - case 'lessThan10': - $qb->andWhere($qb->expr()->lt('p.hoursSpent', $qb->expr()->literal(10))); - break; - case 'upTo20': - $qb->andWhere($qb->expr()->lte('p.hoursSpent', $qb->expr()->literal(20))); - break; - case 'moreThan2weeks': - $qb->andWhere($qb->expr()->gte('p.hoursSpent', $qb->expr()->literal(80))); - break; - case 'overDeadline': - $qb->andWhere($qb->expr()->gt('p.hoursSpent', 'p.deadline')); - break; - } - break; - } - } - - /** - * @Method("GET") - * @Template - * @Route("/", name="homepage") - */ - public function indexAction(Request $request) - { - $qb = $this->getDoctrine()->getManager()->getRepository("AppBundle:Project") - ->createQueryBuilder('p') - ->addSelect('l') - ->innerJoin('p.language', 'l'); - - $options = [ - 'sorters' => ['l.code' => 'ASC'], // sorted by language code by default - 'filters' => ['p.hoursSpent' => 'overDeadline'], // we can apply a filter option by default - 'applyFilter' => [$this, 'projectFilters'], // custom filter handling - ]; - - // our language filter options, the key will be used in where statemt by default - // and the value as title. - // The $filterAny key is a placeholder to skip the filter, so the any value could be ok. - $languages = [ - Pagination::$filterAny => 'Any', - 'php' => 'PHP', - 'hs' => 'Haskell', - 'go' => 'Golang', - ]; - - // our spent time filter options, has specific keys so we know how to customize - $spentTimeGroups = [ - Pagination::$filterAny => 'Any', - 'lessThan10' => 'Less than 10h', - 'upTo20' => 'Up to 20h', - 'moreThan2weeks' => 'More than 2weeks', - 'overDeadline' => 'Over deadline', - ]; - - $projects = new Pagination($qb, $request, $options); - return compact('projects', 'languages', 'spentTimeGroups'); - } -} -``` - -Now here we have added three filters: - -`$languages` and `$spentTimeGroups` will be used as `filter_select` options. The language options are simple and they -refer to direct values, so the where statement does not need to be modified. But spent time groups are custom so we -use custom options. In that case we need an `applyFilter` option to be set as a callable so the QueryBuilder could be modified -accordingly based on our custom options. - -So how the view has changed: - -``` twig - - - - - - - - - - - - - - - - - - - - {% for project in projects %} - - - - - {% if project.isOverDeadline %} - - {% else %} - - {% endif %} - - - {% endfor %} - -
#{{ sorter_link(projects, "p.code", "Code") }}{{ sorter_link(projects, "p.name", "Name") }}{{ sorter_link(projects, "p.hoursSpent", "Hours Spent") }}{{ sorter_link(projects, "l.code", "Language") }}
{{ filter_search(projects, "p.name") }}{{ filter_select(projects, "p.hoursSpent", spentTimeGroups) }}{{ filter_select(projects, "l.code", languages) }}
{{ project.id }}{{ project.code }}{{ project.name }}{{ project.hoursSpent }}{{ project.hoursSpent }}{{ project.language.code }}
- - -``` - -We have used two new twig functions for filters: - -- **filter_search** - for searching projects by name. -- **filter_select** - for basic option filters. - -These functions are rendering twig templates for our filters. - -### Links - -In case if you need to make a link and maintain search filters and sorters applied, use the `$pagination->query()` -function to get all the necessary url parameters and merge it with your link parameters. - -The demo example handles **enable** and **disable** toggling for projects in a separate controller action -and maintains all pagination properties. - -## Configuration - -There is no necessary configuration for a general usage. But in order to customize pagination -there may be global options set in **app.php** for example: - -``` php - 15, - 'range' => 9, -]); -Pagination::$maxPerPage = 200; - -require_once __DIR__.'/../app/AppKernel.php'; - -$kernel = new AppKernel('prod', false); -$kernel->loadClassCache(); -$request = Request::createFromGlobals(); -$response = $kernel->handle($request); -$response->send(); -$kernel->terminate($request, $response); -``` - -### Templates - -The default templates for filters and pagination are based on [twitter bootstrap](http://getbootstrap.com/) and -[fontawesome](http://fortawesome.github.io/Font-Awesome/). You -can customize them same as any other bundle template, for example: - -- pagination - **app/Resources/DataDogPagerBundle/views/pagination.html.twig** -- search filter - **app/Resources/DataDogPagerBundle/views/filters/search.html.twig** - -### Extending with more filters - -The best way to customize your filters is to extend twig extension, or create a new extension. -If we would provide many options, that would confuse people in the end, so instead we add a little boilerplate. -In your bundle **services.yml** update parameters: - -``` yaml -parameters: - datadog.pager.twig_extension.class: AppBundle\Twig\PaginationExtension -``` - -Then create a class: - -``` php - ['html'], - 'needs_environment' => true, - ]; - - $funcs = parent::getFunctions(); - $funcs['filter_search_placeholder'] = new \Twig_Function_Method($this, 'filterSearchPlaceholder', $defaults); - - return $funcs; - } - - public function filterSearchPlaceholder(\Twig_Environment $twig, Pagination $pagination, $key, $placeholder) - { - $value = isset($pagination->query()['filters'][$key]) ? $pagination->query()['filters'][$key] : ''; - return $twig->render('AppBundle::filters/search_placeholder.html.twig', compact('key', 'pagination', 'value', 'placeholder')); - } -} -``` - -And finally copy and modify the template based on your needs +**audit** entities will be mapped automatically if you run schema update or similar. +And all the database changes will be reflected in the audit log afterwards. ## Screenshots -![Screenshot](https://raw.github.com/DATA-DOG/DataDogPagerBundle/master/screenshots/pagination1.png) +![Screenshot](https://raw.github.com/DATA-DOG/DataDogAuditBundle/master/screenshots/audit1.png) + +![Screenshot](https://raw.github.com/DATA-DOG/DataDogAuditBundle/master/screenshots/audit2.png) -![Screenshot](https://raw.github.com/DATA-DOG/DataDogPagerBundle/master/screenshots/pagination2.png) +![Screenshot](https://raw.github.com/DATA-DOG/DataDogAuditBundle/master/screenshots/audit3.png) ## License diff --git a/screenshots/audit1.png b/screenshots/audit1.png new file mode 100644 index 0000000..bfebe20 Binary files /dev/null and b/screenshots/audit1.png differ diff --git a/screenshots/audit2.png b/screenshots/audit2.png new file mode 100644 index 0000000..4d5964c Binary files /dev/null and b/screenshots/audit2.png differ diff --git a/screenshots/audit3.png b/screenshots/audit3.png new file mode 100644 index 0000000..6f767b6 Binary files /dev/null and b/screenshots/audit3.png differ diff --git a/src/DataDog/AuditBundle/Resources/config/doctrine/Association.orm.xml b/src/DataDog/AuditBundle/Resources/config/doctrine/Association.orm.xml new file mode 100644 index 0000000..2f09732 --- /dev/null +++ b/src/DataDog/AuditBundle/Resources/config/doctrine/Association.orm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/src/DataDog/AuditBundle/Resources/config/doctrine/Association.orm.yml b/src/DataDog/AuditBundle/Resources/config/doctrine/Association.orm.yml new file mode 100644 index 0000000..844722d --- /dev/null +++ b/src/DataDog/AuditBundle/Resources/config/doctrine/Association.orm.yml @@ -0,0 +1,20 @@ +DataDog\AuditBundle\Entity\Association: + type: entity + table: audit_associations + id: + id: + type: bigint + generator: + strategy: IDENTITY + + fields: + typ: + length: 128 + tbl: + length: 128 + label: + nullable: true + fk: + type: string + class: + type: string diff --git a/src/DataDog/AuditBundle/Resources/config/doctrine/AuditLog.orm.xml b/src/DataDog/AuditBundle/Resources/config/doctrine/AuditLog.orm.xml new file mode 100644 index 0000000..261ae89 --- /dev/null +++ b/src/DataDog/AuditBundle/Resources/config/doctrine/AuditLog.orm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DataDog/AuditBundle/Resources/config/doctrine/AuditLog.orm.yml b/src/DataDog/AuditBundle/Resources/config/doctrine/AuditLog.orm.yml new file mode 100644 index 0000000..4aaa2cb --- /dev/null +++ b/src/DataDog/AuditBundle/Resources/config/doctrine/AuditLog.orm.yml @@ -0,0 +1,29 @@ +DataDog\AuditBundle\Entity\AuditLog: + type: entity + table: audit_logs + id: + id: + type: bigint + generator: + strategy: IDENTITY + + oneToOne: + source: + targetEntity: Association + joinColumn: + nullable: false + target: + targetEntity: Association + blame: + targetEntity: Association + + fields: + action: + length: 12 + tbl: + length: 128 + diff: + type: json_array + nullable: true + loggedAt: + type: datetime