1 Friendica Storage Backend Addon development
2 ===========================================
6 Storage backends can be added via addons.
7 A storage backend is implemented as a class, and the plugin register the class to make it available to the system.
9 ## The Storage Backend Class
11 The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon.
13 There are two different interfaces you need to implement.
15 ### `ICanWriteToStorage`
17 The class must implement `Friendica\Core\Storage\Capability\ICanWriteToStorage` interface. All method in the interface must be implemented:
20 namespace Friendica\Core\Storage\Capability\ICanWriteToStorage;
22 interface ICanWriteToStorage
24 public function get(string $reference);
25 public function put(string $data, string $reference = '');
26 public function delete(string $reference);
27 public function __toString();
28 public static function getName();
32 - `get(string $reference)` returns data pointed by `$reference`
33 - `put(string $data, string $reference)` saves data in `$data` to position `$reference`, or a new position if `$reference` is empty.
34 - `delete(string $reference)` delete data pointed by `$reference`
36 ### `ICanConfigureStorage`
38 Each storage backend can have options the admin can set in admin page.
39 To make the options possible, you need to implement the `Friendica\Core\Storage\Capability\ICanConfigureStorage` interface.
41 All methods in the interface must be implemented:
44 namespace Friendica\Core\Storage\Capability\ICanConfigureStorage;
46 interface ICanConfigureStorage
48 public function getOptions();
49 public function saveOptions(array $data);
53 - `getOptions()` returns an array with details about each option to build the interface.
54 - `saveOptions(array $data)` get `$data` from admin page, validate it and save it.
56 The array returned by `getOptions()` is defined as:
59 'option1name' => [ ..info.. ],
60 'option2name' => [ ..info.. ],
64 An empty array can be returned if backend doesn't have any options.
66 The info array for each option is defined as:
71 define the field used in form, and the type of data.
72 one of 'checkbox', 'combobox', 'custom', 'datetime', 'input', 'intcheckbox', 'password', 'radio', 'richtext', 'select', 'select_raw', 'textarea'
76 Translatable label of the field. This label will be shown in admin page
80 Current value of the option
84 Translatable description for the field. Will be shown in admin page
88 Optional. Depends on which 'type' this option is:
90 - 'select': array `[ value => label ]` of choices
91 - 'intcheckbox': value of input element
92 - 'select_raw': prebuild html string of `<option >` tags
94 Each label should be translatable
99 See doxygen documentation of `IWritableStorage` interface for details about each method.
101 ## Register a storage backend class
103 Each backend must be registered in the system when the plugin is installed, to be aviable.
105 `DI::facStorage()->register(string $class)` is used to register the backend class.
107 When the plugin is uninstalled, registered backends must be unregistered using
108 `DI::facStorage()->unregister(string $class)`.
110 You have to register a new hook in your addon, listening on `storage_instance(App $a, array $data)`.
111 In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Core\Storage\Capability\ICanReadFromStorage` class.
112 Set the instance of your class as `$data['storage']` to pass it back to the backend.
114 This is necessary because it isn't always clear, if you need further construction arguments.
118 **Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
120 Each new Storage class should be added to the test-environment at [Storage Tests](https://github.com/friendica/friendica/tree/develop/tests/src/Model/Storage/).
122 Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
124 Override the two necessary instances:
127 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
129 abstract class StorageTest
131 // returns an instance of your newly created storage class
132 abstract protected function getInstance();
134 // Assertion for the option array you return for your new StorageClass
135 abstract protected function assertOption(ICanWriteToStorage $storage);
139 ## Exception handling
141 There are two intended types of exceptions for storages
143 ### `ReferenceStorageExecption`
145 This storage exception should be used in case the caller tries to use an invalid references.
146 This could happen in case the caller tries to delete or update an unknown reference.
147 The implementation of the storage backend must not ignore invalid references.
149 Avoid throwing the common `StorageExecption` instead of the `ReferenceStorageException` at this particular situation!
151 ### `StorageException`
153 This is the common exception in case unexpected errors happen using the storage backend.
154 If there's a predecessor to this exception (e.g. you caught an exception and are throwing this execption), you should add the predecessor for transparency reasons.
159 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
161 class ExampleStorage implements ICanWriteToStorage
163 public function get(string $reference) : string
166 throw new Exception('a real bad exception');
167 } catch (Exception $exception) {
168 throw new \Friendica\Core\Storage\Exception\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception);
176 Here is a hypothetical addon which register a useless storage backend.
177 Let's call it `samplestorage`.
179 This backend will discard all data we try to save and will return always the same image when we ask for some data.
180 The image returned can be set by the administrator in admin page.
182 First, the backend class.
183 The file will be `addon/samplestorage/SampleStorageBackend.php`:
187 namespace Friendica\Addon\samplestorage;
189 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
191 use Friendica\Core\Config\Capability\IManageConfigValues;
192 use Friendica\Core\L10n;
194 class SampleStorageBackend implements ICanWriteToStorage
196 const NAME = 'Sample Storage';
202 * SampleStorageBackend constructor.
204 * You can add here every dynamic class as dependency you like and add them to a private field
205 * Friendica automatically creates these classes and passes them as argument to the constructor
207 public function __construct(string $filename)
209 $this->filename = $filename;
212 public function get(string $reference)
214 // we return always the same image data. Which file we load is defined by
216 return file_get_contents($this->filename);
219 public function put(string $data, string $reference = '')
221 if ($reference === '') {
222 $reference = 'sample';
224 // we don't save $data !
228 public function delete(string $reference)
230 // we pretend to delete the data
234 public function __toString()
239 public static function getName()
248 namespace Friendica\Addon\samplestorage;
250 use Friendica\Core\Storage\Capability\ICanConfigureStorage;
252 use Friendica\Core\Config\Capability\IManageConfigValues;
253 use Friendica\Core\L10n;
255 class SampleStorageBackendConfig implements ICanConfigureStorage
257 /** @var \Friendica\Core\Config\Capability\IManageConfigValues */
263 * SampleStorageBackendConfig constructor.
265 * You can add here every dynamic class as dependency you like and add them to a private field
266 * Friendica automatically creates these classes and passes them as argument to the constructor
268 public function __construct(IManageConfigValues $config, L10n $l10n)
270 $this->config = $config;
274 public function getFileName(): string
276 return $this->config->get('storage', 'samplestorage', 'sample.jpg');
279 public function getOptions()
281 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
284 'input', // will use a simple text input
285 $this->l10n->t('The file to return'), // the label
286 $filename, // the current value
287 $this->l10n->t('Enter the path to a file'), // the help text
288 // no extra data for 'input' type..
293 public function saveOptions(array $data)
295 // the keys in $data are the same keys we defined in getOptions()
296 $newfilename = trim($data['filename']);
298 // this function should always validate the data.
299 // in this example we check if file exists
300 if (!file_exists($newfilename)) {
301 // in case of error we return an array with
302 // ['optionname' => 'error message']
303 return ['filename' => 'The file doesn\'t exists'];
306 $this->config->set('storage', 'samplestorage', $newfilename);
308 // no errors, return empty array
315 Now the plugin main file. Here we register and unregister the backend class.
317 The file is `addon/samplestorage/samplestorage.php`
322 * Name: Sample Storage Addon
323 * Description: A sample addon which implements an unusefull storage backend
325 * Author: Alice <https://alice.social/~alice>
328 use Friendica\Addon\samplestorage\SampleStorageBackend;
329 use Friendica\Addon\samplestorage\SampleStorageBackendConfig;
332 function samplestorage_install()
334 Hook::register('storage_instance' , __FILE__, 'samplestorage_storage_instance');
335 Hook::register('storage_config' , __FILE__, 'samplestorage_storage_config');
336 DI::storageManager()->register(SampleStorageBackend::class);
339 function samplestorage_storage_uninstall()
341 DI::storageManager()->unregister(SampleStorageBackend::class);
344 function samplestorage_storage_instance(App $a, array &$data)
346 $config = new SampleStorageBackendConfig(DI::l10n(), DI::config());
347 $data['storage'] = new SampleStorageBackendConfig($config->getFileName());
350 function samplestorage_storage_config(App $a, array &$data)
352 $data['storage_config'] = new SampleStorageBackendConfig(DI::l10n(), DI::config());
357 **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
360 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
361 use Friendica\Test\src\Core\Storage\StorageTest;
363 class SampleStorageTest extends StorageTest
365 // returns an instance of your newly created storage class
366 protected function getInstance()
368 // create a new SampleStorageBackend instance with all it's dependencies
369 // Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
370 return new SampleStorageBackend();
373 // Assertion for the option array you return for your new StorageClass
374 protected function assertOption(ICanWriteToStorage $storage)
376 $this->assertEquals([
379 'The file to return',
381 'Enter the path to a file'
383 ], $storage->getOptions());