{"id":1885,"date":"2021-08-12T00:00:00","date_gmt":"2021-08-11T22:00:00","guid":{"rendered":"https:\/\/wwwneu.strehle.de\/tim\/weblog\/archives\/2021\/08\/12\/1644-2\/"},"modified":"2025-07-31T21:35:17","modified_gmt":"2025-07-31T19:35:17","slug":"1644-2","status":"publish","type":"post","link":"https:\/\/www.strehle.de\/tim\/weblog\/archives\/2021\/08\/12\/1644-2\/","title":{"rendered":"A Companion for WoodWing Assets"},"content":{"rendered":"\n<p>At the <a href=\"https:\/\/www.spiegelgruppe.de\">SPIEGEL-Verlag<\/a>, we use the <a href=\"https:\/\/www.woodwing.com\/en\/digital-asset-management-system\">WoodWing Assets<\/a> DAM software in production since January 2020. Most of the images you see on <a href=\"https:\/\/www.spiegel.de\">spiegel.de<\/a> (one of Germany\u2019s largest news sites) are sent to our <a href=\"https:\/\/polygon.rocks\">Web CMS<\/a> from Assets.<\/p>\n\n\n\n<p><strong>WoodWing Assets<\/strong> is a pretty good DAM product, focused on a relatively small set of core features. There is little built-in business logic or connectors \u2013 instead, Assets provides a couple of <strong>extension mechanisms<\/strong> so you can customize it: <a href=\"https:\/\/helpcenter.woodwing.com\/hc\/en-us\/articles\/360041853552\">Action plugins<\/a> and <a href=\"https:\/\/helpcenter.woodwing.com\/hc\/en-us\/articles\/360041853932\">Panel plugins<\/a> (custom HTML + JavaScript embedded into the Assets UI), a <a href=\"https:\/\/helpcenter.woodwing.com\/hc\/en-us\/sections\/360008455892-APIs-REST\">REST API<\/a>, <a href=\"https:\/\/helpcenter.woodwing.com\/hc\/en-us\/articles\/360041901972-Working-with-Webhooks-in-Assets-Server\">Webhooks<\/a>, and <a href=\"https:\/\/helpcenter.woodwing.com\/hc\/en-us\/articles\/360041853372-Routing-API-requests-through-Assets-Server-using-API-plug-ins\">API plugins<\/a> (a wrapper for custom REST services).<\/p>\n\n\n\n<p>We made use of action and panel plugins to add functionality to the Assets UI (like dialogs for \u201cadvanced search\u201d and Web CMS export). But there is also a bunch of \u201cinvisible\u201d logic. Here are a few of our <strong>use cases<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>moving assets into the right Assets folder after upload into a collection<\/li>\n\n\n\n<li>reading IPTC Credit and Creator fields (on import or update), and combining them in a \u201ccalculated credit\u201d metadata field, ready to use in our CMS<\/li>\n\n\n\n<li>updating (via the CMS API) the image credit shown on the spiegel.de Web site when the credit is changed within Assets<\/li>\n\n\n\n<li>depublishing images from the Web site when the expiry date is reached<\/li>\n\n\n\n<li>routing files through OneVision Amendo for automatic image optimization<\/li>\n<\/ul>\n\n\n\n<p>Since we were dealing with multiple workflows and systems, we knew that simple point-to-point integrations would become difficult to coordinate and maintain. We decided to bundle all of that logic into a dedicated service, and <strong>developed a Web service application we call the \u201cCompanion\u201d<\/strong>. Other systems are allowed to use the Assets REST API directly for read-only purposes, but all data modification and Assets Webhook handling is done exclusively within the Companion.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/s3.eu-central-1.amazonaws.com\/files.strehle.de\/tim\/blog\/2020-09-30+Assets+Companion+as+a+black+box.png\"><img decoding=\"async\" src=\"https:\/\/s3.eu-central-1.amazonaws.com\/files.strehle.de\/tim\/blog\/2020-09-30+Assets+Companion+as+a+black+box.png\" alt=\"Diagram: Companion as a black box\"\/><\/a><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>The Companion is <strong>written in PHP<\/strong> and has no UI. Its main interfaces are a Webhook receiver and a small API. It builds upon <a href=\"\/tim\/weblog\/archives\/2020\/05\/20\/1643\">the Assets client PHP library we open sourced<\/a>, but the Companion itself is not open source (still, let us know if you\u2019re interested in using it).<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/s3.eu-central-1.amazonaws.com\/files.strehle.de\/tim\/blog\/2020-09-30+Assets+Companion+Whats+in+the+box.png\"><img decoding=\"async\" src=\"https:\/\/s3.eu-central-1.amazonaws.com\/files.strehle.de\/tim\/blog\/2020-09-30+Assets+Companion+Whats+in+the+box.png\" alt=\"Diagram: What\u2019s in the box\"\/><\/a><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>The <strong>Webhook receiver<\/strong> decides which actions should be triggered by an incoming Assets event (we can easily plug in code for new use cases using <a href=\"https:\/\/symfony.com\/doc\/current\/event_dispatcher.html\">Symfony Event Subscribers<\/a>). Any action involving another system is not executed immediately, but added to a RabbitMQ queue (or a <a href=\"https:\/\/camunda.com\">Camunda<\/a> instance for more complex workflows) and <strong>processed asynchronously<\/strong> by worker processes (using the <a href=\"https:\/\/symfony.com\/doc\/current\/components\/messenger.html\">Symfony Messenger<\/a>). We use one queue and one set of worker processes per integrated system so we can scale processes individually, and stop them when that system has a downtime.<\/p>\n\n\n\n<p>We run the Companion in an <strong>OpenShift<\/strong> cluster (based on <strong>Docker \/ Kubernetes<\/strong>). Deployment and maintenance are super simple, and there even is a nice UI with 1-click scaling \ud83d\ude09<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/s3.eu-central-1.amazonaws.com\/files.strehle.de\/tim\/blog\/2020-09-30+Assets+Companion+OpenShift+Console.png\"><img decoding=\"async\" src=\"https:\/\/s3.eu-central-1.amazonaws.com\/files.strehle.de\/tim\/blog\/2020-09-30+Assets+Companion+OpenShift+Console.png\" alt=\"OpenShift Web Console\"\/><\/a><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Want to see some code? This <strong>code snippet running in the Webhook receiver<\/strong> catches all \u201cfile created\u201d events and adds a \u201ccheck for duplicates\u201d event to RabbitMQ:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\nclass AssetLinkDuplicatesSubscriber extends AssetsWebhookSubscriber\n{\n    public static function getSubscribedEvents()\n    {\n        return &#91;\n            AssetCreateEvent::class =&gt; 'handle',\n            AssetCreateByCopyEvent::class =&gt; 'handle',\n            AssetCreateFromFilestoreRescueEvent::class =&gt; 'handle',\n            AssetCreateFromVersionEvent::class =&gt; 'handle'\n        ];\n    }\n\n    public function handle(AssetsWebhookEvent $assetsWebhookEvent): void\n    {\n        \/\/ Skip variations\n        if (strlen($assetsWebhookEvent-&gt;getMasterId()) &gt; 0) {\n            return;\n        }\n\n        \/\/ ignore if this is a container (e.g. collection)\n        if ($assetsWebhookEvent-&gt;getAssetDomain() === 'container') {\n            return;\n        }\n\n        $this-&gt;messageBus-&gt;dispatch(new AssetLinkDuplicatesMessage($assetsWebhookEvent));\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The <strong>worker process<\/strong> runs this code to perform the actual work, searching Assets for identical files and creating relations between the assets:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\nclass AssetLinkDuplicatesHandler implements MessageHandlerInterface\n{\n    const RELATION_TYPE = 'duplicate';\n\n    protected AssetsClient $assetsClient;\n    protected LoggerInterface $logger;\n\n    public function __construct(LoggerInterface $logger, AssetsClient $assetsClient)\n    {\n        $this-&gt;assetsClient = $assetsClient;\n        $this-&gt;logger = $logger;\n    }\n\n    public function __invoke(AssetLinkDuplicatesMessage $message): void\n    {\n        $assetsWebhookEvent = $message-&gt;getAssetsWebhookEvent();\n\n        \/\/ Skip variations\n        if (strlen($assetsWebhookEvent-&gt;getMasterId()) &gt; 0) {\n            return;\n        }\n\n        $asset = $this-&gt;assetsClient-&gt;searchAsset($assetsWebhookEvent-&gt;getAssetId());\n\n        try {\n            if (empty($asset-&gt;getMetadata()&#91;'firstExtractedChecksum'])) {\n                throw new RuntimeException(sprintf('%s: Failed to find the firstExtractedChecksum for asset %s',\n                    __METHOD__, $asset-&gt;getId()));\n            }\n\n            \/\/ check if there are other (original) assets with the same firstExtractedChecksum\n\n            $query = sprintf(\n                'firstExtractedChecksum:%s -id:%s -masterId:*',\n                $asset-&gt;getMetadata()&#91;'firstExtractedChecksum'],\n                $asset-&gt;getId()\n            );\n\n            $response = $this-&gt;assetsClient-&gt;search((new SearchRequest($this-&gt;assetsClient-&gt;getConfig()))\n                -&gt;setQ($query));\n\n            if ($response-&gt;getTotalHits() === 0) {\n                \/\/ no assets to relate\n                return;\n            }\n\n            foreach ($response-&gt;getHits() as $hit) {\n                $relationRequest = new CreateRelationRequest($this-&gt;assetsClient-&gt;getConfig());\n                $relationRequest-&gt;setTarget1Id($asset-&gt;getId());\n                $relationRequest-&gt;setTarget2Id($hit-&gt;getId());\n                $relationRequest-&gt;setRelationType(self::RELATION_TYPE);\n                $this-&gt;assetsClient-&gt;createRelation($relationRequest);\n            }\n\n        } catch (RuntimeException $e) {\n            throw new RuntimeException(sprintf('%s: Failed to create duplicate relations for asset %s %s', __METHOD__,\n                $asset-&gt;getId(), $e-&gt;getMessage()), $e-&gt;getCode(), $e);\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The Companion has been running in production since our Assets launch one and a half years ago, and so far it has been proven to be fast, very <strong>stable<\/strong> (the downtimes we had were related to the OpenShift environment), and <strong>easily adapted<\/strong> to new requirements. We\u2019re very happy with it \ud83d\ude42<\/p>\n","protected":false},"excerpt":{"rendered":"<p>At the SPIEGEL-Verlag, we use the WoodWing Assets DAM software in production since January 2020. Most of the images you see on spiegel.de (one of Germany\u2019s largest news sites) are sent to our Web CMS from Assets. WoodWing Assets is a pretty good DAM product, focused on a relatively small set of core features. There [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_share_on_mastodon":"0"},"categories":[1],"tags":[],"class_list":["post-1885","post","type-post","status-publish","format-standard","hentry","category-weblog"],"share_on_mastodon":{"url":"","error":""},"_links":{"self":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts\/1885","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/comments?post=1885"}],"version-history":[{"count":1,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts\/1885\/revisions"}],"predecessor-version":[{"id":1889,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts\/1885\/revisions\/1889"}],"wp:attachment":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/media?parent=1885"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/categories?post=1885"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/tags?post=1885"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}