Skip to content

Commit 161bc17

Browse files
Merge pull request shopware#911 from shopware/next-9496/delete-unused-media-extension
NEXT-9496 - Document how to extend the unused media deletion process
2 parents 12e628e + 7fbfa11 commit 161bc17

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
* [Add data to mails](guides/plugins/plugins/content/mail/add-data-to-mails.md)
171171
* [Add mail templates](guides/plugins/plugins/content/mail/add-mail-template.md)
172172
* [Media](guides/plugins/plugins/content/media/README.md)
173+
* [Prevent Deletion of Media Files Referenced in your Plugins](guides/plugins/plugins/content/media/prevent-deletion-of-media-files-referenced-in-your-plugins.md)
173174
* [Add custom media extension](guides/plugins/plugins/content/media/add-custom-file-extension.md)
174175
* [SEO](guides/plugins/plugins/content/seo/README.md)
175176
* [Add custom SEO URLs](guides/plugins/plugins/content/seo/add-custom-seo-url.md)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Prevent Deletion of Media Files Referenced in your Plugins
2+
3+
{% hint style="info" %}
4+
The ability to prevent Media entities from being deleted is available since Shopware 6.5.1.0.
5+
{% endhint %}
6+
7+
## Overview
8+
9+
The Shopware CLI application provides a `media:delete-unused` command which deletes all media entities and their corresponding files which are not used in your application.
10+
Not used means that it is not referenced by any other entity. This works well in the simple case that all your entity definitions store references to Media entities with correct foreign keys.
11+
12+
However, this does not cover all the possible cases, even for many internal Shopware features. For example the CMS entities store their configuration as JSON blobs with references to Media IDs stored in a nested data structure.
13+
14+
In order to fix the case of Media references that cannot be resolved without knowledge of the specific entity and its features, an extension point is provided via an event.
15+
16+
If you are developing an extension which references Media entities, and you cannot use foreign keys, this guide will detail how to prevent shopware deleting the Media entities your extension references.
17+
18+
## Prerequisites
19+
20+
As most of our plugin guides, this guide was also built upon our [Plugin base guide](../../plugin-base-guide.md).
21+
Furthermore, you'll have to know about adding classes to the [Dependency injection](../../plugin-fundamentals/dependency-injection.md) container
22+
and about using a subscriber in order to [Listen to events](../../plugin-fundamentals/listening-to-events.md).
23+
24+
## The deletion process
25+
26+
The `\Shopware\Core\Content\Media\UnusedMediaPurger` service first searches for Media entities that are not referenced by any other entities in the system via foreign keys. Then it dispatches an event containing the Media IDs it believes are unused.
27+
28+
The event is an instance of `\Shopware\Core\Content\Media\Event\UnusedMediaSearchEvent`. A subscriber can then cross-reference the Media IDs scheduled to be deleted and mark any of them as *used*.
29+
30+
The remaining Media IDs will then be deleted by the `\Shopware\Core\Content\Media\UnusedMediaPurger` service.
31+
32+
Please note that this process is completed in small batches to maintain stability, so the event may be dispatched multiple times when an installation has many unused Media entities.
33+
34+
## Adding a subscriber
35+
36+
In this section, we're going to register a subscriber for the `\Shopware\Core\Content\Media\Event\UnusedMediaSearchEvent` event.
37+
38+
Have a look at the following code example:
39+
{% code title="<plugin root>/src/Subscriber/UnusedMediaSubscriber.php" %}
40+
41+
```php
42+
<?php declare(strict_types=1);
43+
44+
namespace Swag\BasicExample\Subscriber;
45+
46+
use Shopware\Core\Content\Media\Event\UnusedMediaSearchEvent;
47+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
48+
49+
class UnusedMediaSubscriber implements EventSubscriberInterface
50+
{
51+
public static function getSubscribedEvents(): array
52+
{
53+
return [
54+
UnusedMediaSearchEvent::class => 'removeUsedMedia',
55+
];
56+
}
57+
58+
public function removeUsedMedia(UnusedMediaSearchEvent $event): void
59+
{
60+
$idsToBeDeleted = $event->getUnusedIds();
61+
62+
$doNotDeleteTheseIds = $this->getUsedMediaIds($idsToBeDeleted);
63+
64+
$event->markAsUsed($doNotDeleteTheseIds);
65+
}
66+
67+
private function getUsedMediaIds(array $idsToBeDeleted): array
68+
{
69+
// do something to get the IDs that are used
70+
return [];
71+
}
72+
}
73+
```
74+
75+
{% endcode %}
76+
77+
You can use the method `getUnusedIds` of the `$event` variable to get the current an array of Media IDs scheduled for removal.
78+
79+
You can use these IDs to query whatever storage your plugin uses to store references to Media entities, to check if they are currently used.
80+
81+
If any of the IDs are used by your plugin, you can use the method `markAsUsed` of the `$event` variable to prevent the Media entities from being deleted. `markAsUsed` accepts an array of string IDs.
82+
83+
If your storage is a relational database such as MySQL you should, when possible, use direct database queries to check for references. This saves memory and CPU cycles by not loading unnecessary data.
84+
85+
Imagine an extension which provides an image slider feature. An implementation of `getUsedMediaIds` might look something like the following:
86+
87+
{% code title="<plugin root>/src/Subscriber/UnusedMediaSubscriber.php" %}
88+
89+
```php
90+
private function getUsedMediaIds(array $idsToBeDeleted): array
91+
{
92+
$sql = <<<SQL
93+
SELECT JSON_EXTRACT(slider_config, "$.images") as mediaIds FROM my_slider_table
94+
WHERE JSON_OVERLAPS(
95+
JSON_EXTRACT(slider_config, "$.images"),
96+
JSON_ARRAY(?)
97+
);
98+
SQL;
99+
100+
$usedMediaIds = $this->connection->fetchFirstColumn(
101+
$sql,
102+
[$event->getUnusedIds()],
103+
[ArrayParameterType::STRING]
104+
);
105+
106+
return array_map(fn (string $ids) => json_decode($ids, true, \JSON_THROW_ON_ERROR), $usedMediaIds);
107+
}
108+
```
109+
110+
{% endcode %}
111+
112+
In the above example, `$this->connection` is an instance of `\Doctrine\DBAL\Connection` which can be injected in to your subscriber.
113+
We use the MySQL JSON functions to query the table `my_slider_table`.
114+
We check if there are any references to the Media IDs from the event, in the `slider_config` column which is a JSON blob. The `JSON_EXTRACT` function looks into the `images` key of the data. We use the where condition in combination with the `JSON_OVERLAPS` function to only query rows that have references to the Media IDs we are interested in.
115+
116+
Finally, we return all the IDs of Media which are used in the slider config so that they are not deleted.
117+
118+
Make sure to register your event subscriber to the [Dependency injection container](../../plugin-fundamentals/dependency-injection.md)
119+
by using the tag `kernel.event_subscriber`.
120+
121+
{% tab title="services.xml" %}
122+
{% code title="<plugin root>/src/Resources/config/services.xml" %}
123+
124+
```xml
125+
<?xml version="1.0" ?>
126+
<container xmlns="http://symfony.com/schema/dic/services"
127+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
128+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
129+
130+
<services>
131+
<service id="Swag\BasicExample\Subscriber\UnusedMediaSubscriber">
132+
<tag name="kernel.event_subscriber"/>
133+
</service>
134+
</services>
135+
</container>
136+
```
137+
138+
{% endtab %}
139+
{% endtabs %}

0 commit comments

Comments
 (0)