Move L10n class from L10n subdir to Core (replacing old wrapper)
[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 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\IStorage` interface. All method in the interface must be implemented:
14
15 namespace Friendica\Model\Storage;
16
17 ```php
18 interface IStorage
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', 'yesno'
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 - 'yesno': array `[ 'label no', 'label yes']`
77
78 Each label should be translatable
79
80         ];
81
82
83 See doxygen documentation of `IStorage` interface for details about each method.
84
85 ## Register a storage backend class
86
87 Each backend must be registered in the system when the plugin is installed, to be aviable.
88
89 `DI::facStorage()->register(string $class)` is used to register the backend class.
90
91 When the plugin is uninstalled, registered backends must be unregistered using
92 `DI::facStorage()->unregister(string $class)`.
93
94 You have to register a new hook in your addon, listening on `storage_instance(App $a, array $data)`.
95 In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Model\Storage\IStorage` class.
96 Set the instance of your class as `$data['storage']` to pass it back to the backend.
97
98 This is necessary because it isn't always clear, if you need further construction arguments.
99
100 ## Adding tests
101
102 **Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
103
104 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
106 Add a new test class which's naming convention is `StorageClassTest`, which extend the `StorageTest` in the same directory.
107
108 Override the two necessary instances:
109 ```php
110 use Friendica\Model\Storage\IStorage;
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(IStorage $storage);
119
120 ```
121
122 ## Example
123
124 Here an hypotetical addon which register an unusefull storage backend.
125 Let's call it `samplestorage`.
126
127 This backend will discard all data we try to save and will return always the same image when we ask for some data.
128 The image returned can be set by the administrator in admin page.
129
130 First, the backend class.
131 The file will be `addon/samplestorage/SampleStorageBackend.php`:
132
133 ```php
134 <?php
135 namespace Friendica\Addon\samplestorage;
136
137 use Friendica\Model\Storage\IStorage;
138
139 use Friendica\Core\Config;
140 use Friendica\Core\L10n;
141
142 class SampleStorageBackend implements IStorage
143 {
144         const NAME = 'Sample Storage';
145
146         /** @var Config\IConfiguration */
147         private $config;
148         /** @var \Friendica\Core\L10n */
149         private $l10n;
150
151         /**
152           * SampleStorageBackend constructor.
153           * @param Config\IConfiguration $config The configuration of Friendica
154           *                                                                       
155           * You can add here every dynamic class as dependency you like and add them to a private field
156           * Friendica automatically creates these classes and passes them as argument to the constructor                                                                           
157           */
158         public function __construct(Config\IConfiguration $config, \Friendica\Core\L10n $l10n) 
159         {
160                 $this->config = $config;
161                 $this->l10n   = $l10n;
162         }
163
164         public function get(string $reference)
165         {
166                 // we return always the same image data. Which file we load is defined by
167                 // a config key
168                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
169                 return file_get_contents($filename);
170         }
171         
172         public function put(string $data, string $reference = '')
173         {
174                 if ($reference === '') {
175                         $reference = 'sample';
176                 }
177                 // we don't save $data !
178                 return $reference;
179         }
180         
181         public function delete(string $reference)
182         {
183                 // we pretend to delete the data
184                 return true;
185         }
186         
187         public function getOptions()
188         {
189                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
190                 return [
191                         'filename' => [
192                                 'input',        // will use a simple text input
193                                 $this->l10n->t('The file to return'),   // the label
194                                 $filename,      // the current value
195                                 $this->l10n->t('Enter the path to a file'), // the help text
196                                 // no extra data for 'input' type..
197                         ],
198                 ];
199         }
200         
201         public function saveOptions(array $data)
202         {
203                 // the keys in $data are the same keys we defined in getOptions()
204                 $newfilename = trim($data['filename']);
205                 
206                 // this function should always validate the data.
207                 // in this example we check if file exists
208                 if (!file_exists($newfilename)) {
209                         // in case of error we return an array with
210                         // ['optionname' => 'error message']
211                         return ['filename' => 'The file doesn\'t exists'];
212                 }
213                 
214                 $this->config->set('storage', 'samplestorage', $newfilename);
215                 
216                 // no errors, return empty array
217                 return [];
218         }
219
220         public function __toString()
221         {
222                 return self::NAME;
223         }
224
225         public static function getName()
226         {
227                 return self::NAME;
228         }
229 }
230 ```
231
232 Now the plugin main file. Here we register and unregister the backend class.
233
234 The file is `addon/samplestorage/samplestorage.php`
235
236 ```php
237 <?php
238 /**
239  * Name: Sample Storage Addon
240  * Description: A sample addon which implements an unusefull storage backend
241  * Version: 1.0.0
242  * Author: Alice <https://alice.social/~alice>
243  */
244
245 use Friendica\Addon\samplestorage\SampleStorageBackend;
246 use Friendica\DI;
247
248 function samplestorage_install()
249 {
250         // on addon install, we register our class with name "Sample Storage".
251         // note: we use `::class` property, which returns full class name as string
252         // this save us the problem of correctly escape backslashes in class name
253         DI::storageManager()->register(SampleStorageBackend::class);
254 }
255
256 function samplestorage_unistall()
257 {
258         // when the plugin is uninstalled, we unregister the backend.
259         DI::storageManager()->unregister(SampleStorageBackend::class);
260 }
261
262 function samplestorage_storage_instance(\Friendica\App $a, array $data)
263 {
264     if ($data['name'] === SampleStorageBackend::getName()) {
265     // instance a new sample storage instance and pass it back to the core for usage
266         $data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
267     }
268 }
269 ```
270
271 **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
272
273 ```php
274 use Friendica\Model\Storage\IStorage;
275 use Friendica\Test\src\Model\Storage\StorageTest;
276
277 class SampleStorageTest extends StorageTest 
278 {
279         // returns an instance of your newly created storage class
280         protected function getInstance()
281         {
282                 // create a new SampleStorageBackend instance with all it's dependencies
283                 // Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
284                 return new SampleStorageBackend();
285         }
286
287         // Assertion for the option array you return for your new StorageClass
288         protected function assertOption(IStorage $storage)
289         {
290                 $this->assertEquals([
291                         'filename' => [
292                                 'input',
293                                 'The file to return',
294                                 'sample.jpg',
295                                 'Enter the path to a file'
296                         ],
297                 ], $storage->getOptions());
298         }
299
300 ```