]> git.mxchange.org Git - friendica.git/blob - doc/AddonStorageBackend.md
Fix commenting on public posts
[friendica.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 avaiable 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 The class must implement `Friendica\Model\Storage\IWritableStorage` interface. All method in the interface must be implemented:
14
15 namespace Friendica\Model\IWritableStorage;
16
17 ```php
18 interface IWritableStorage
19 {
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();
27 }
28 ```
29
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`
33
34 Each storage backend can have options the admin can set in admin page.
35
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.
38
39 The array returned by `getOptions()` is defined as:
40
41         [
42                 'option1name' => [ ..info.. ],
43                 'option2name' => [ ..info.. ],
44                 ...
45         ]
46
47 An empty array can be returned if backend doesn't have any options.
48
49 The info array for each option is defined as:
50
51         [
52                 'type',
53
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'
56
57                 'label',
58
59 Translatable label of the field. This label will be shown in admin page
60
61                 value,
62
63 Current value of the option
64
65                 'help text',
66
67 Translatable description for the field. Will be shown in admin page
68
69                 extra data
70
71 Optional. Depends on which 'type' this option is:
72
73 - 'select': array `[ value => label ]` of choices
74 - 'intcheckbox': value of input element
75 - 'select_raw': prebuild html string of `<option >` tags
76
77 Each label should be translatable
78
79         ];
80
81
82 See doxygen documentation of `IWritableStorage` interface for details about each method.
83
84 ## Register a storage backend class
85
86 Each backend must be registered in the system when the plugin is installed, to be aviable.
87
88 `DI::facStorage()->register(string $class)` is used to register the backend class.
89
90 When the plugin is uninstalled, registered backends must be unregistered using
91 `DI::facStorage()->unregister(string $class)`.
92
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.
96
97 This is necessary because it isn't always clear, if you need further construction arguments.
98
99 ## Adding tests
100
101 **Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
102
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/).
104
105 Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
106
107 Override the two necessary instances:
108
109 ```php
110 use Friendica\Model\Storage\IWritableStorage;
111
112 abstract class StorageTest 
113 {
114         // returns an instance of your newly created storage class
115         abstract protected function getInstance();
116
117         // Assertion for the option array you return for your new StorageClass
118         abstract protected function assertOption(IWritableStorage $storage);
119
120 ```
121
122 ## Exception handling
123
124 There are two intended types of exceptions for storages
125
126 ### `ReferenceStorageExecption`
127
128 This storage exception should be used in case the caller tries to use an invalid references.
129 This could happen in case the caller tries to delete or update an unknown reference.
130 The implementation of the storage backend must not ignore invalid references.
131
132 Avoid throwing the common `StorageExecption` instead of the `ReferenceStorageException` at this particular situation!
133
134 ### `StorageException`
135
136 This is the common exception in case unexpected errors happen using the storage backend.
137 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.
138
139 Example:
140
141 ```php
142 use Friendica\Model\Storage\IWritableStorage;
143
144 class ExampleStorage implements IWritableStorage 
145 {
146         public function get(string $reference) : string
147         {
148                 try {
149                         throw new Exception('a real bad exception');
150                 } catch (Exception $exception) {
151                         throw new \Friendica\Model\Storage\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception);
152                 }
153         }
154
155 ```
156
157 ## Example
158
159 Here an hypotetical addon which register a useless storage backend.
160 Let's call it `samplestorage`.
161
162 This backend will discard all data we try to save and will return always the same image when we ask for some data.
163 The image returned can be set by the administrator in admin page.
164
165 First, the backend class.
166 The file will be `addon/samplestorage/SampleStorageBackend.php`:
167
168 ```php
169 <?php
170 namespace Friendica\Addon\samplestorage;
171
172 use Friendica\Model\Storage\IWritableStorage;
173
174 use Friendica\Core\Config\IConfig;
175 use Friendica\Core\L10n;
176
177 class SampleStorageBackend implements IWritableStorage
178 {
179         const NAME = 'Sample Storage';
180
181         /** @var IConfig */
182         private $config;
183         /** @var L10n */
184         private $l10n;
185
186         /**
187           * SampleStorageBackend constructor.
188           * @param IConfig $config The configuration of Friendica
189           *                                                                       
190           * You can add here every dynamic class as dependency you like and add them to a private field
191           * Friendica automatically creates these classes and passes them as argument to the constructor                                                                           
192           */
193         public function __construct(IConfig $config, L10n $l10n) 
194         {
195                 $this->config = $config;
196                 $this->l10n   = $l10n;
197         }
198
199         public function get(string $reference)
200         {
201                 // we return always the same image data. Which file we load is defined by
202                 // a config key
203                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
204                 return file_get_contents($filename);
205         }
206         
207         public function put(string $data, string $reference = '')
208         {
209                 if ($reference === '') {
210                         $reference = 'sample';
211                 }
212                 // we don't save $data !
213                 return $reference;
214         }
215         
216         public function delete(string $reference)
217         {
218                 // we pretend to delete the data
219                 return true;
220         }
221         
222         public function getOptions()
223         {
224                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
225                 return [
226                         'filename' => [
227                                 'input',        // will use a simple text input
228                                 $this->l10n->t('The file to return'),   // the label
229                                 $filename,      // the current value
230                                 $this->l10n->t('Enter the path to a file'), // the help text
231                                 // no extra data for 'input' type..
232                         ],
233                 ];
234         }
235         
236         public function saveOptions(array $data)
237         {
238                 // the keys in $data are the same keys we defined in getOptions()
239                 $newfilename = trim($data['filename']);
240                 
241                 // this function should always validate the data.
242                 // in this example we check if file exists
243                 if (!file_exists($newfilename)) {
244                         // in case of error we return an array with
245                         // ['optionname' => 'error message']
246                         return ['filename' => 'The file doesn\'t exists'];
247                 }
248                 
249                 $this->config->set('storage', 'samplestorage', $newfilename);
250                 
251                 // no errors, return empty array
252                 return [];
253         }
254
255         public function __toString()
256         {
257                 return self::NAME;
258         }
259
260         public static function getName()
261         {
262                 return self::NAME;
263         }
264 }
265 ```
266
267 Now the plugin main file. Here we register and unregister the backend class.
268
269 The file is `addon/samplestorage/samplestorage.php`
270
271 ```php
272 <?php
273 /**
274  * Name: Sample Storage Addon
275  * Description: A sample addon which implements an unusefull storage backend
276  * Version: 1.0.0
277  * Author: Alice <https://alice.social/~alice>
278  */
279
280 use Friendica\Addon\samplestorage\SampleStorageBackend;
281 use Friendica\DI;
282
283 function samplestorage_install()
284 {
285         // on addon install, we register our class with name "Sample Storage".
286         // note: we use `::class` property, which returns full class name as string
287         // this save us the problem of correctly escape backslashes in class name
288         DI::storageManager()->register(SampleStorageBackend::class);
289 }
290
291 function samplestorage_unistall()
292 {
293         // when the plugin is uninstalled, we unregister the backend.
294         DI::storageManager()->unregister(SampleStorageBackend::class);
295 }
296
297 function samplestorage_storage_instance(\Friendica\App $a, array $data)
298 {
299     if ($data['name'] === SampleStorageBackend::getName()) {
300     // instance a new sample storage instance and pass it back to the core for usage
301         $data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
302     }
303 }
304 ```
305
306 **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
307
308 ```php
309 use Friendica\Model\Storage\IWritableStorage;
310 use Friendica\Test\src\Model\Storage\StorageTest;
311
312 class SampleStorageTest extends StorageTest 
313 {
314         // returns an instance of your newly created storage class
315         protected function getInstance()
316         {
317                 // create a new SampleStorageBackend instance with all it's dependencies
318                 // Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
319                 return new SampleStorageBackend();
320         }
321
322         // Assertion for the option array you return for your new StorageClass
323         protected function assertOption(IWritableStorage $storage)
324         {
325                 $this->assertEquals([
326                         'filename' => [
327                                 'input',
328                                 'The file to return',
329                                 'sample.jpg',
330                                 'Enter the path to a file'
331                         ],
332                 ], $storage->getOptions());
333         }
334
335 ```