Improvements for channel counter
[friendica.git/.git] / doc / AddonStorageBackend.md
1 Friendica Storage Backend Addon development
2 ===========================================
3
4 * [Home](help)
5
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.
8
9 ## The Storage Backend Class
10
11 The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon.
12
13 There are two different interfaces you need to implement.
14
15 ### `ICanWriteToStorage`
16
17 The class must implement `Friendica\Core\Storage\Capability\ICanWriteToStorage` interface. All method in the interface must be implemented:
18
19 ```php
20 namespace Friendica\Core\Storage\Capability\ICanWriteToStorage;
21
22 interface ICanWriteToStorage
23 {
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();
29 }
30 ```
31
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`
35
36 ### `ICanConfigureStorage`
37
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.
40
41 All methods in the interface must be implemented:
42
43 ```php
44 namespace Friendica\Core\Storage\Capability\ICanConfigureStorage;
45
46 interface ICanConfigureStorage
47 {
48         public function getOptions();
49         public function saveOptions(array $data);
50 }
51 ```
52
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.
55
56 The array returned by `getOptions()` is defined as:
57
58         [
59                 'option1name' => [ ..info.. ],
60                 'option2name' => [ ..info.. ],
61                 ...
62         ]
63
64 An empty array can be returned if backend doesn't have any options.
65
66 The info array for each option is defined as:
67
68         [
69                 'type',
70
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'
73
74                 'label',
75
76 Translatable label of the field. This label will be shown in admin page
77
78                 value,
79
80 Current value of the option
81
82                 'help text',
83
84 Translatable description for the field. Will be shown in admin page
85
86                 extra data
87
88 Optional. Depends on which 'type' this option is:
89
90 - 'select': array `[ value => label ]` of choices
91 - 'intcheckbox': value of input element
92 - 'select_raw': prebuild html string of `<option >` tags
93
94 Each label should be translatable
95
96         ];
97
98
99 See doxygen documentation of `IWritableStorage` interface for details about each method.
100
101 ## Register a storage backend class
102
103 Each backend must be registered in the system when the plugin is installed, to be available.
104
105 `DI::facStorage()->register(string $class)` is used to register the backend class.
106
107 When the plugin is uninstalled, registered backends must be unregistered using
108 `DI::facStorage()->unregister(string $class)`.
109
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.
113
114 This is necessary because it isn't always clear, if you need further construction arguments.
115
116 ## Adding tests
117
118 **Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
119
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/).
121
122 Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
123
124 Override the two necessary instances:
125
126 ```php
127 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
128
129 abstract class StorageTest 
130 {
131         // returns an instance of your newly created storage class
132         abstract protected function getInstance();
133
134         // Assertion for the option array you return for your new StorageClass
135         abstract protected function assertOption(ICanWriteToStorage $storage);
136
137 ```
138
139 ## Exception handling
140
141 There are two intended types of exceptions for storages
142
143 ### `ReferenceStorageException`
144
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.
148
149 Avoid throwing the common `StorageException` instead of the `ReferenceStorageException` at this particular situation!
150
151 ### `StorageException`
152
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 exception), you should add the predecessor for transparency reasons.
155
156 Example:
157
158 ```php
159 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
160
161 class ExampleStorage implements ICanWriteToStorage 
162 {
163         public function get(string $reference) : string
164         {
165                 try {
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);
169                 }
170         }
171
172 ```
173
174 ## Example
175
176 Here is a hypothetical addon which register a useless storage backend.
177 Let's call it `samplestorage`.
178
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.
181
182 First, the backend class.
183 The file will be `addon/samplestorage/SampleStorageBackend.php`:
184
185 ```php
186 <?php
187 namespace Friendica\Addon\samplestorage;
188
189 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
190
191 use Friendica\Core\Config\Capability\IManageConfigValues;
192 use Friendica\Core\L10n;
193
194 class SampleStorageBackend implements ICanWriteToStorage
195 {
196         const NAME = 'Sample Storage';
197
198         /** @var string */
199         private $filename;
200
201         /**
202           * SampleStorageBackend constructor.
203           * 
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                                                                           
206           */
207         public function __construct(string $filename) 
208         {
209                 $this->filename = $filename;
210         }
211
212         public function get(string $reference)
213         {
214                 // we return always the same image data. Which file we load is defined by
215                 // a config key
216                 return file_get_contents($this->filename);
217         }
218         
219         public function put(string $data, string $reference = '')
220         {
221                 if ($reference === '') {
222                         $reference = 'sample';
223                 }
224                 // we don't save $data !
225                 return $reference;
226         }
227         
228         public function delete(string $reference)
229         {
230                 // we pretend to delete the data
231                 return true;
232         }
233         
234         public function __toString()
235         {
236                 return self::NAME;
237         }
238
239         public static function getName()
240         {
241                 return self::NAME;
242         }
243 }
244 ```
245
246 ```php
247 <?php
248 namespace Friendica\Addon\samplestorage;
249
250 use Friendica\Core\Storage\Capability\ICanConfigureStorage;
251
252 use Friendica\Core\Config\Capability\IManageConfigValues;
253 use Friendica\Core\L10n;
254
255 class SampleStorageBackendConfig implements ICanConfigureStorage
256 {
257         /** @var \Friendica\Core\Config\Capability\IManageConfigValues */
258         private $config;
259         /** @var L10n */
260         private $l10n;
261
262         /**
263           * SampleStorageBackendConfig constructor.
264           * 
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                                                                           
267           */
268         public function __construct(IManageConfigValues $config, L10n $l10n) 
269         {
270                 $this->config = $config;
271                 $this->l10n   = $l10n;
272         }
273
274         public function getFileName(): string
275         {
276                 return $this->config->get('storage', 'samplestorage', 'sample.jpg');
277         }
278
279         public function getOptions()
280         {
281                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
282                 return [
283                         'filename' => [
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..
289                         ],
290                 ];
291         }
292         
293         public function saveOptions(array $data)
294         {
295                 // the keys in $data are the same keys we defined in getOptions()
296                 $newfilename = trim($data['filename']);
297                 
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'];
304                 }
305                 
306                 $this->config->set('storage', 'samplestorage', $newfilename);
307                 
308                 // no errors, return empty array
309                 return [];
310         }
311
312 }
313 ```
314
315 Now the plugin main file. Here we register and unregister the backend class.
316
317 The file is `addon/samplestorage/samplestorage.php`
318
319 ```php
320 <?php
321 /**
322  * Name: Sample Storage Addon
323  * Description: A sample addon which implements a very limited storage backend
324  * Version: 1.0.0
325  * Author: Alice <https://alice.social/~alice>
326  */
327
328 use Friendica\Addon\samplestorage\SampleStorageBackend;
329 use Friendica\Addon\samplestorage\SampleStorageBackendConfig;
330 use Friendica\DI;
331
332 function samplestorage_install()
333 {
334         Hook::register('storage_instance' , __FILE__, 'samplestorage_storage_instance');
335         Hook::register('storage_config' , __FILE__, 'samplestorage_storage_config');
336         DI::storageManager()->register(SampleStorageBackend::class);
337 }
338
339 function samplestorage_storage_uninstall()
340 {
341         DI::storageManager()->unregister(SampleStorageBackend::class);
342 }
343
344 function samplestorage_storage_instance(App $a, array &$data)
345 {
346         $config          = new SampleStorageBackendConfig(DI::l10n(), DI::config());
347         $data['storage'] = new SampleStorageBackendConfig($config->getFileName());
348 }
349
350 function samplestorage_storage_config(App $a, array &$data)
351 {
352         $data['storage_config'] = new SampleStorageBackendConfig(DI::l10n(), DI::config());
353 }
354
355 ```
356
357 **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
358
359 ```php
360 use Friendica\Core\Storage\Capability\ICanWriteToStorage;
361 use Friendica\Test\src\Core\Storage\StorageTest;
362
363 class SampleStorageTest extends StorageTest 
364 {
365         // returns an instance of your newly created storage class
366         protected function getInstance()
367         {
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();
371         }
372
373         // Assertion for the option array you return for your new StorageClass
374         protected function assertOption(ICanWriteToStorage $storage)
375         {
376                 $this->assertEquals([
377                         'filename' => [
378                                 'input',
379                                 'The file to return',
380                                 'sample.jpg',
381                                 'Enter the path to a file'
382                         ],
383                 ], $storage->getOptions());
384         }
385
386 ```