1"use strict";(globalThis.webpackChunkflarum_docs=globalThis.webpackChunkflarum_docs||[]).push([[1113],{5017(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"extend/audit","title":"Integrating with Audit","description":"The flarum/audit extension records moderation and administration actions to a tamper-resistant log. It exposes a public extender, Flarum\\\\Audit\\\\Extend\\\\Audit, so any extension can record its own actions without modifying Audit \u2014 the integration lives in the extension that owns the events.","source":"@site/docs/extend/audit.md","sourceDirName":"extend","slug":"/extend/audit","permalink":"/extend/audit","draft":false,"unlisted":false,"editUrl":"https://github.com/flarum/docs/tree/main/docs/extend/audit.md","tags":[],"version":"current","frontMatter":{},"sidebar":"extendSidebar","previous":{"title":"Realtime","permalink":"/extend/realtime"},"next":{"title":"Post Types","permalink":"/extend/post-types"}}');var a=t(4848),s=t(8453);const r={},o="Integrating with Audit",d={},c=[{value:"Declaring the optional dependency",id:"declaring-the-optional-dependency",level:2},{value:"Listening to events",id:"listening-to-events",level:2},{value:"Stateful integrations",id:"stateful-integrations",level:2},{value:"Recording the actor explicitly",id:"recording-the-actor-explicitly",level:2},{value:"Extender reference",id:"extender-reference",level:2},{value:"Translating actions",id:"translating-actions",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.header,{children:(0,a.jsx)(n.h1,{id:"integrating-with-audit",children:"Integrating with Audit"})}),"\n",(0,a.jsxs)(n.p,{children:["The ",(0,a.jsxs)(n.a,{href:"/extensions/audit",children:[(0,a.jsx)(n.code,{children:"flarum/audit"})," extension"]})," records moderation and administration actions to a tamper-resistant log. It exposes a public extender, ",(0,a.jsx)(n.code,{children:"Flarum\\Audit\\Extend\\Audit"}),", so ",(0,a.jsx)(n.strong,{children:"any extension can record its own actions"})," without modifying Audit \u2014 the integration lives in the extension that owns the events."]}),"\n",(0,a.jsxs)(n.p,{children:["This is how every bundled extension (and, increasingly, the third-party ones) adds its audit coverage. For the catalogue of actions already recorded, see the ",(0,a.jsx)(n.a,{href:"/extensions/audit#logged-actions",children:"Audit extension page"}),"."]}),"\n",(0,a.jsx)(n.admonition,{title:"Optional dependency",type:"info",children:(0,a.jsxs)(n.p,{children:["Audit is an optional extension. All integration code must be guarded so your extension continues to work when Audit is not installed. The patterns on this page show the correct way to do this \u2014 the same approach used for ",(0,a.jsx)(n.a,{href:"/extend/realtime",children:"Realtime"}),"."]})}),"\n",(0,a.jsx)(n.h2,{id:"declaring-the-optional-dependency",children:"Declaring the optional dependency"}),"\n",(0,a.jsxs)(n.p,{children:["Always wrap the extender in ",(0,a.jsx)(n.code,{children:"Extend\\Conditional()->whenExtensionEnabled('flarum-audit', ...)"}),". The closure is only evaluated when Audit is installed and enabled, so your extension keeps working \u2014 and never references an Audit class \u2014 when Audit is absent."]}),"\n",(0,a.jsxs)(n.p,{children:["Declare ",(0,a.jsx)(n.code,{children:"flarum/audit"})," as an optional dependency in your ",(0,a.jsx)(n.code,{children:"composer.json"})," so it is recognised and loaded in the right order when present:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-json",children:'{\n "extra": {\n "flarum-extension": {\n "optional-dependencies": [\n "flarum/audit"\n ]\n }\n }\n}\n'})}),"\n",(0,a.jsx)(n.h2,{id:"listening-to-events",children:"Listening to events"}),"\n",(0,a.jsxs)(n.p,{children:["The common case is logging an event. ",(0,a.jsx)(n.code,{children:"listen()"})," registers the action automatically and stores whatever array your callback returns (return ",(0,a.jsx)(n.code,{children:"null"})," to skip a particular occurrence):"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-php",children:"use Acme\\MyExtension\\Event\\WidgetCreated;\nuse Flarum\\Audit\\Extend\\Audit;\nuse Flarum\\Extend;\n\nreturn [\n (new Extend\\Conditional())\n ->whenExtensionEnabled('flarum-audit', fn () => [\n (new Audit())\n ->listen(WidgetCreated::class, 'acme.widget_created', fn (WidgetCreated $event) => [\n 'widget_id' => $event->widget->id,\n ]),\n ]),\n];\n"})}),"\n",(0,a.jsx)(n.p,{children:"The actor and IP are taken from the current request automatically \u2014 you only return the action-specific payload."}),"\n",(0,a.jsx)(n.h2,{id:"stateful-integrations",children:"Stateful integrations"}),"\n",(0,a.jsxs)(n.p,{children:["When the event you need does not exist, or you must capture some state before a change happens, pass an invokable object to ",(0,a.jsx)(n.code,{children:"using()"}),". It receives the ",(0,a.jsx)(n.a,{href:"/extend/start",children:"container"})," once the application has booted and is responsible for calling ",(0,a.jsx)(n.code,{children:"Flarum\\Audit\\AuditLogger::log()"})," itself. If the object exposes a public static ",(0,a.jsx)(n.code,{children:"$actions"})," array, those actions are registered automatically:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-php",children:"namespace Acme\\MyExtension;\n\nuse Acme\\MyExtension\\Event\\WidgetSaving;\nuse Flarum\\Audit\\AuditLogger;\nuse Illuminate\\Contracts\\Container\\Container;\nuse Illuminate\\Contracts\\Events\\Dispatcher;\n\nclass WidgetAuditIntegration\n{\n public static array $actions = ['acme.widget_renamed'];\n\n public function __invoke(Container $container): void\n {\n $events = $container->
1make(Dispatcher::class);\n\n $events->listen(WidgetSaving::class, function (WidgetSaving $event) {\n if ($event->widget->isDirty('name')) {\n AuditLogger::log('acme.widget_renamed', [\n 'widget_id' => $event->widget->id,\n 'old_name' => $event->widget->getOriginal('name'),\n 'new_name' => $event->widget->name,\n ]);\n }\n });\n }\n}\n"})}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-php",children:"(new Audit())\n ->using(new WidgetAuditIntegration()),\n"})}),"\n",(0,a.jsx)(n.h2,{id:"recording-the-actor-explicitly",children:"Recording the actor explicitly"}),"\n",(0,a.jsxs)(n.p,{children:["Audit attributes each entry to the actor of the current request. When you log from a ",(0,a.jsx)(n.a,{href:"/queue",children:"queued job"})," \u2014 where there is no request \u2014 that actor is not available, so set it yourself before logging by assigning ",(0,a.jsx)(n.code,{children:"AuditLogger::$actor"}),":"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-php",children:"use Flarum\\Audit\\AuditLogger;\nuse Flarum\\User\\User;\n\n// In a job/listener, attribute the entry to a specific user (or null for a system action):\nAuditLogger::$actor = $processedBy ? User::find($processedBy) : null;\n\nAuditLogger::log('acme.something_processed', ['user_id' => $subject->id]);\n"})}),"\n",(0,a.jsx)(n.p,{children:"This is exactly how the GDPR integration records the administrator who processed an erasure, and falls back to no actor for a scheduled, system-driven erasure."}),"\n",(0,a.jsx)(n.h2,{id:"extender-reference",children:"Extender reference"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.code,{children:"register(string ...$actions)"})," \u2014 declare action strings so they appear in the admin limited-access settings and in ",(0,a.jsx)(n.code,{children:"action:"})," search autocomplete, without binding a listener (useful for actions logged from middleware or commands)."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.code,{children:"listen(string $event, string $action, callable $payload)"})," \u2014 listen to an event and log ",(0,a.jsx)(n.code,{children:"$action"}),". The callback receives the event and returns the payload array, or ",(0,a.jsx)(n.code,{children:"null"})," to skip. The action is registered automatically."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.code,{children:"using(callable $callback)"})," \u2014 an escape hatch for advanced cases (such as model lifecycle hooks) that receives the container once the application has booted."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.code,{children:"group(?string $group)"})," \u2014 set the extension grouping shown in the admin settings. Defaults to the extension declaring the integration."]}),"\n"]}),"\n",(0,a.jsx)(n.h2,{id:"translating-actions",children:"Translating actions"}),"\n",(0,a.jsxs)(n.p,{children:["Add a translation for each action so the browser shows a readable sentence instead of the raw payload. The key is ",(0,a.jsx)(n.code,{children:"flarum-audit.lib.browser.<action>"})," (a dotted action such as ",(0,a.jsx)(n.code,{children:"acme.widget_created"})," nests accordingly), and any payload key is available as a ",(0,a.jsx)(n.code,{children:"{placeholder}"}),". Define it under the ",(0,a.jsx)(n.code,{children:"flarum-audit"})," namespace in your own extension's ",(0,a.jsx)(n.a,{href:"/extend/i18n",children:"locale file"})," so it travels with your integration:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-yaml",children:"flarum-audit:\n lib:\n browser:\n acme:\n widget_created: Created widget {widget_id}\n widget_renamed: Renamed widget from {old_name} to {new_name}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(l,{...e})}):l(e)}},8453(e,n,t){t.d(n,{R:()=>r,x:()=>o});var i=t(6540);const a={},s=i.createContext(a);function r(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.