Merge pull request #10165 from nupplaphil/bug/strip_pageinfo
[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'
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 `IStorage` 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 ```php
109 use Friendica\Model\Storage\IStorage;
110
111 abstract class StorageTest 
112 {
113         // returns an instance of your newly created storage class
114         abstract protected function getInstance();
115
116         // Assertion for the option array you return for your new StorageClass
117         abstract protected function assertOption(IStorage $storage);
118
119 ```
120
121 ## Example
122
123 Here an hypotetical addon which register an unusefull storage backend.
124 Let's call it `samplestorage`.
125
126 This backend will discard all data we try to save and will return always the same image when we ask for some data.
127 The image returned can be set by the administrator in admin page.
128
129 First, the backend class.
130 The file will be `addon/samplestorage/SampleStorageBackend.php`:
131
132 ```php
133 <?php
134 namespace Friendica\Addon\samplestorage;
135
136 use Friendica\Model\Storage\IStorage;
137
138 use Friendica\Core\Config\IConfig;
139 use Friendica\Core\L10n;
140
141 class SampleStorageBackend implements IStorage
142 {
143         const NAME = 'Sample Storage';
144
145         /** @var IConfig */
146         private $config;
147         /** @var L10n */
148         private $l10n;
149
150         /**
151           * SampleStorageBackend constructor.
152           * @param IConfig $config The configuration of Friendica
153           *                                                                       
154           * You can add here every dynamic class as dependency you like and add them to a private field
155           * Friendica automatically creates these classes and passes them as argument to the constructor                                                                           
156           */
157         public function __construct(IConfig $config, L10n $l10n) 
158         {
159                 $this->config = $config;
160                 $this->l10n   = $l10n;
161         }
162
163         public function get(string $reference)
164         {
165                 // we return always the same image data. Which file we load is defined by
166                 // a config key
167                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
168                 return file_get_contents($filename);
169         }
170         
171         public function put(string $data, string $reference = '')
172         {
173                 if ($reference === '') {
174                         $reference = 'sample';
175                 }
176                 // we don't save $data !
177                 return $reference;
178         }
179         
180         public function delete(string $reference)
181         {
182                 // we pretend to delete the data
183                 return true;
184         }
185         
186         public function getOptions()
187         {
188                 $filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
189                 return [
190                         'filename' => [
191                                 'input',        // will use a simple text input
192                                 $this->l10n->t('The file to return'),   // the label
193                                 $filename,      // the current value
194                                 $this->l10n->t('Enter the path to a file'), // the help text
195                                 // no extra data for 'input' type..
196                         ],
197                 ];
198         }
199         
200         public function saveOptions(array $data)
201         {
202                 // the keys in $data are the same keys we defined in getOptions()
203                 $newfilename = trim($data['filename']);
204                 
205                 // this function should always validate the data.
206                 // in this example we check if file exists
207                 if (!file_exists($newfilename)) {
208                         // in case of error we return an array with
209                         // ['optionname' => 'error message']
210                         return ['filename' => 'The file doesn\'t exists'];
211                 }
212                 
213                 $this->config->set('storage', 'samplestorage', $newfilename);
214                 
215                 // no errors, return empty array
216                 return [];
217         }
218
219         public function __toString()
220         {
221                 return self::NAME;
222         }
223
224         public static function getName()
225         {
226                 return self::NAME;
227         }
228 }
229 ```
230
231 Now the plugin main file. Here we register and unregister the backend class.
232
233 The file is `addon/samplestorage/samplestorage.php`
234
235 ```php
236 <?php
237 /**
238  * Name: Sample Storage Addon
239  * Description: A sample addon which implements an unusefull storage backend
240  * Version: 1.0.0
241  * Author: Alice <https://alice.social/~alice>
242  */
243
244 use Friendica\Addon\samplestorage\SampleStorageBackend;
245 use Friendica\DI;
246
247 function samplestorage_install()
248 {
249         // on addon install, we register our class with name "Sample Storage".
250         // note: we use `::class` property, which returns full class name as string
251         // this save us the problem of correctly escape backslashes in class name
252         DI::storageManager()->register(SampleStorageBackend::class);
253 }
254
255 function samplestorage_unistall()
256 {
257         // when the plugin is uninstalled, we unregister the backend.
258         DI::storageManager()->unregister(SampleStorageBackend::class);
259 }
260
261 function samplestorage_storage_instance(\Friendica\App $a, array $data)
262 {
263     if ($data['name'] === SampleStorageBackend::getName()) {
264     // instance a new sample storage instance and pass it back to the core for usage
265         $data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
266     }
267 }
268 ```
269
270 **Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php`:
271
272 ```php
273 use Friendica\Model\Storage\IStorage;
274 use Friendica\Test\src\Model\Storage\StorageTest;
275
276 class SampleStorageTest extends StorageTest 
277 {
278         // returns an instance of your newly created storage class
279         protected function getInstance()
280         {
281                 // create a new SampleStorageBackend instance with all it's dependencies
282                 // Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
283                 return new SampleStorageBackend();
284         }
285
286         // Assertion for the option array you return for your new StorageClass
287         protected function assertOption(IStorage $storage)
288         {
289                 $this->assertEquals([
290                         'filename' => [
291                                 'input',
292                                 'The file to return',
293                                 'sample.jpg',
294                                 'Enter the path to a file'
295                         ],
296                 ], $storage->getOptions());
297         }
298
299 ```