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 avaiable 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 The class must implement `Friendica\Model\Storage\ISelectableStorage` interface. All method in the interface must be implemented:
15 namespace Friendica\Model\ISelectableStorage;
18 interface ISelectableStorage
20 public function get(string $reference);
21 public function put(string $data, string $reference = '');
22 public function delete(string $reference);
23 public function getOptions();
24 public function saveOptions(array $data);
25 public function __toString();
26 public static function getName();
30 - `get(string $reference)` returns data pointed by `$reference`
31 - `put(string $data, string $reference)` saves data in `$data` to position `$reference`, or a new position if `$reference` is empty.
32 - `delete(string $reference)` delete data pointed by `$reference`
34 Each storage backend can have options the admin can set in admin page.
36 - `getOptions()` returns an array with details about each option to build the interface.
37 - `saveOptions(array $data)` get `$data` from admin page, validate it and save it.
39 The array returned by `getOptions()` is defined as:
42 'option1name' => [ ..info.. ],
43 'option2name' => [ ..info.. ],
47 An empty array can be returned if backend doesn't have any options.
49 The info array for each option is defined as:
54 define the field used in form, and the type of data.
55 one of 'checkbox', 'combobox', 'custom', 'datetime', 'input', 'intcheckbox', 'password', 'radio', 'richtext', 'select', 'select_raw', 'textarea'
59 Translatable label of the field. This label will be shown in admin page
63 Current value of the option
67 Translatable description for the field. Will be shown in admin page
71 Optional. Depends on which 'type' this option is:
73 - 'select': array `[ value => label ]` of choices
74 - 'intcheckbox': value of input element
75 - 'select_raw': prebuild html string of `<option >` tags
77 Each label should be translatable
82 See doxygen documentation of `ISelectableStorage` interface for details about each method.
84 ## Register a storage backend class
86 Each backend must be registered in the system when the plugin is installed, to be aviable.
88 `DI::facStorage()->register(string $class)` is used to register the backend class.
90 When the plugin is uninstalled, registered backends must be unregistered using
91 `DI::facStorage()->unregister(string $class)`.
93 You have to register a new hook in your addon, listening on `storage_instance(App $a, array $data)`.
94 In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Model\Storage\IStorage` class.
95 Set the instance of your class as `$data['storage']` to pass it back to the backend.
97 This is necessary because it isn't always clear, if you need further construction arguments.
101 **Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
103 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/).
105 Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
107 Override the two necessary instances:
109 use Friendica\Model\Storage\ISelectableStorage;
111 abstract class StorageTest
113 // returns an instance of your newly created storage class
114 abstract protected function getInstance();
116 // Assertion for the option array you return for your new StorageClass
117 abstract protected function assertOption(ISelectableStorage $storage);
121 ## Exception handling
123 There are two intended types of exceptions for storages
125 ### `ReferenceStorageExecption`
127 This storage exception should be used in case the caller tries to use an invalid references.
128 This could happen in case the caller tries to delete or update an unknown reference.
129 The implementation of the storage backend must not ignore invalid references.
131 Avoid throwing the common `StorageExecption` instead of the `ReferenceStorageException` at this particular situation!
133 ### `StorageException`
135 This is the common exception in case unexpected errors happen using the storage backend.
136 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.
141 use Friendica\Model\Storage\ISelectableStorage;
143 class ExampleStorage implements ISelectableStorage
145 public function get(string $reference) : string
148 throw new Exception('a real bad exception');
149 } catch (Exception $exception) {
150 throw new \Friendica\Model\Storage\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception);
158 Here an hypotetical addon which register a useless storage backend.
159 Let's call it `samplestorage`.
161 This backend will discard all data we try to save and will return always the same image when we ask for some data.
162 The image returned can be set by the administrator in admin page.
164 First, the backend class.
165 The file will be `addon/samplestorage/SampleStorageBackend.php`:
169 namespace Friendica\Addon\samplestorage;
171 use Friendica\Model\Storage\ISelectableStorage;
173 use Friendica\Core\Config\IConfig;
174 use Friendica\Core\L10n;
176 class SampleStorageBackend implements ISelectableStorage
178 const NAME = 'Sample Storage';
186 * SampleStorageBackend constructor.
187 * @param IConfig $config The configuration of Friendica
189 * You can add here every dynamic class as dependency you like and add them to a private field
190 * Friendica automatically creates these classes and passes them as argument to the constructor
192 public function __construct(IConfig $config, L10n $l10n)
194 $this->config = $config;
198 public function get(string $reference)
200 // we return always the same image data. Which file we load is defined by
202 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
203 return file_get_contents($filename);
206 public function put(string $data, string $reference = '')
208 if ($reference === '') {
209 $reference = 'sample';
211 // we don't save $data !
215 public function delete(string $reference)
217 // we pretend to delete the data
221 public function getOptions()
223 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
226 'input', // will use a simple text input
227 $this->l10n->t('The file to return'), // the label
228 $filename, // the current value
229 $this->l10n->t('Enter the path to a file'), // the help text
230 // no extra data for 'input' type..
235 public function saveOptions(array $data)
237 // the keys in $data are the same keys we defined in getOptions()
238 $newfilename = trim($data['filename']);
240 // this function should always validate the data.
241 // in this example we check if file exists
242 if (!file_exists($newfilename)) {
243 // in case of error we return an array with
244 // ['optionname' => 'error message']
245 return ['filename' => 'The file doesn\'t exists'];
248 $this->config->set('storage', 'samplestorage', $newfilename);
250 // no errors, return empty array
254 public function __toString()
259 public static function getName()
266 Now the plugin main file. Here we register and unregister the backend class.
268 The file is `addon/samplestorage/samplestorage.php`
273 * Name: Sample Storage Addon
274 * Description: A sample addon which implements an unusefull storage backend
276 * Author: Alice <https://alice.social/~alice>
279 use Friendica\Addon\samplestorage\SampleStorageBackend;
282 function samplestorage_install()
284 // on addon install, we register our class with name "Sample Storage".
285 // note: we use `::class` property, which returns full class name as string
286 // this save us the problem of correctly escape backslashes in class name
287 DI::storageManager()->register(SampleStorageBackend::class);
290 function samplestorage_unistall()
292 // when the plugin is uninstalled, we unregister the backend.
293 DI::storageManager()->unregister(SampleStorageBackend::class);
296 function samplestorage_storage_instance(\Friendica\App $a, array $data)
298 if ($data['name'] === SampleStorageBackend::getName()) {
299 // instance a new sample storage instance and pass it back to the core for usage
300 $data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
305 **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
308 use Friendica\Model\Storage\ISelectableStorage;
309 use Friendica\Test\src\Model\Storage\StorageTest;
311 class SampleStorageTest extends StorageTest
313 // returns an instance of your newly created storage class
314 protected function getInstance()
316 // create a new SampleStorageBackend instance with all it's dependencies
317 // Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
318 return new SampleStorageBackend();
321 // Assertion for the option array you return for your new StorageClass
322 protected function assertOption(ISelectableStorage $storage)
324 $this->assertEquals([
327 'The file to return',
329 'Enter the path to a file'
331 ], $storage->getOptions());