2018-12-12 14:36:56 -05:00
Friendica Storage Backend Addon development
===========================================
* [Home ](help )
Storage backends can be added via addons.
A storage backend is implemented as a class, and the plugin register the class to make it avaiable to the system.
## The Storage Backend Class
The class must live in `Friendica\Addon\youraddonname` namespace, where `youraddonname` the folder name of your addon.
The class must implement `Friendica\Model\Storage\IStorage` interface. All method in the interface must be implemented:
namespace Friendica\Model\Storage;
2018-12-12 14:52:09 -05:00
```php
interface IStorage
{
2020-01-05 16:33:27 -05:00
public function get(string $reference);
public function put(string $data, string $reference = '');
public function delete(string $reference);
public function getOptions();
public function saveOptions(array $data);
public function __toString();
2020-01-08 19:48:48 -05:00
public static function getName();
2018-12-12 14:52:09 -05:00
}
```
2018-12-12 14:36:56 -05:00
2020-01-05 16:33:27 -05:00
- `get(string $reference)` returns data pointed by `$reference`
- `put(string $data, string $reference)` saves data in `$data` to position `$reference` , or a new position if `$reference` is empty.
- `delete(string $reference)` delete data pointed by `$reference`
2018-12-12 14:36:56 -05:00
Each storage backend can have options the admin can set in admin page.
- `getOptions()` returns an array with details about each option to build the interface.
2020-01-05 16:33:27 -05:00
- `saveOptions(array $data)` get `$data` from admin page, validate it and save it.
2018-12-12 14:36:56 -05:00
The array returned by `getOptions()` is defined as:
[
'option1name' => [ ..info.. ],
'option2name' => [ ..info.. ],
...
]
An empty array can be returned if backend doesn't have any options.
The info array for each option is defined as:
[
'type',
define the field used in form, and the type of data.
2020-02-05 11:30:35 -05:00
one of 'checkbox', 'combobox', 'custom', 'datetime', 'input', 'intcheckbox', 'password', 'radio', 'richtext', 'select', 'select_raw', 'textarea'
2018-12-12 14:36:56 -05:00
'label',
Translatable label of the field. This label will be shown in admin page
value,
Current value of the option
'help text',
Translatable description for the field. Will be shown in admin page
extra data
Optional. Depends on which 'type' this option is:
- 'select': array `[ value => label ]` of choices
- 'intcheckbox': value of input element
- 'select_raw': prebuild html string of `<option >` tags
Each label should be translatable
];
See doxygen documentation of `IStorage` interface for details about each method.
## Register a storage backend class
Each backend must be registered in the system when the plugin is installed, to be aviable.
2020-01-08 16:51:37 -05:00
`DI::facStorage()->register(string $class)` is used to register the backend class.
2018-12-12 14:36:56 -05:00
When the plugin is uninstalled, registered backends must be unregistered using
2020-01-08 16:51:37 -05:00
`DI::facStorage()->unregister(string $class)` .
You have to register a new hook in your addon, listening on `storage_instance(App $a, array $data)` .
In case `$data['name']` is your storage class name, you have to instance a new instance of your `Friendica\Model\Storage\IStorage` class.
Set the instance of your class as `$data['storage']` to pass it back to the backend.
This is necessary because it isn't always clear, if you need further construction arguments.
2020-01-05 16:33:27 -05:00
## Adding tests
**Currently testing is limited to core Friendica only, this shows theoretically how tests should work in the future**
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/ ).
Add a new test class which's naming convention is `StorageClassTest` , which extend the `StorageTest` in the same directory.
Override the two necessary instances:
```php
use Friendica\Model\Storage\IStorage;
abstract class StorageTest
{
// returns an instance of your newly created storage class
abstract protected function getInstance();
// Assertion for the option array you return for your new StorageClass
abstract protected function assertOption(IStorage $storage);
}
```
2018-12-12 14:36:56 -05:00
## Example
Here an hypotetical addon which register an unusefull storage backend.
Let's call it `samplestorage` .
This backend will discard all data we try to save and will return always the same image when we ask for some data.
The image returned can be set by the administrator in admin page.
First, the backend class.
The file will be `addon/samplestorage/SampleStorageBackend.php` :
```php
< ?php
namespace Friendica\Addon\samplestorage;
use Friendica\Model\Storage\IStorage;
2020-01-19 15:29:36 -05:00
use Friendica\Core\Config\IConfig;
2018-12-12 14:36:56 -05:00
use Friendica\Core\L10n;
class SampleStorageBackend implements IStorage
{
2020-01-05 16:33:27 -05:00
const NAME = 'Sample Storage';
2020-01-19 15:29:36 -05:00
/** @var IConfig */
2020-01-05 16:33:27 -05:00
private $config;
2020-01-18 16:07:07 -05:00
/** @var L10n */
2020-01-05 16:33:27 -05:00
private $l10n;
/**
* SampleStorageBackend constructor.
2020-01-19 15:29:36 -05:00
* @param IConfig $config The configuration of Friendica
2020-01-05 16:33:27 -05:00
*
* You can add here every dynamic class as dependency you like and add them to a private field
* Friendica automatically creates these classes and passes them as argument to the constructor
*/
2020-01-19 15:29:36 -05:00
public function __construct(IConfig $config, L10n $l10n)
2020-01-05 16:33:27 -05:00
{
$this->config = $config;
$this->l10n = $l10n;
}
public function get(string $reference)
2018-12-12 14:36:56 -05:00
{
2020-01-05 16:33:27 -05:00
// we return always the same image data. Which file we load is defined by
2018-12-12 14:36:56 -05:00
// a config key
2020-01-05 16:33:27 -05:00
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
2018-12-12 14:36:56 -05:00
return file_get_contents($filename);
}
2020-01-05 16:33:27 -05:00
public function put(string $data, string $reference = '')
2018-12-12 14:36:56 -05:00
{
2020-01-05 16:33:27 -05:00
if ($reference === '') {
$reference = 'sample';
2018-12-12 14:36:56 -05:00
}
// we don't save $data !
2020-01-05 16:33:27 -05:00
return $reference;
2018-12-12 14:36:56 -05:00
}
2020-01-05 16:33:27 -05:00
public function delete(string $reference)
2018-12-12 14:36:56 -05:00
{
// we pretend to delete the data
return true;
}
2020-01-05 16:33:27 -05:00
public function getOptions()
2018-12-12 14:36:56 -05:00
{
2020-01-05 16:33:27 -05:00
$filename = $this->config->get('storage', 'samplestorage', 'sample.jpg');
2018-12-12 14:36:56 -05:00
return [
2020-01-05 16:33:27 -05:00
'filename' => [
'input', // will use a simple text input
$this->l10n->t('The file to return'), // the label
2018-12-12 14:36:56 -05:00
$filename, // the current value
2020-01-05 16:33:27 -05:00
$this->l10n->t('Enter the path to a file'), // the help text
// no extra data for 'input' type..
],
2018-12-12 14:36:56 -05:00
];
}
2020-01-05 16:33:27 -05:00
public function saveOptions(array $data)
2018-12-12 14:36:56 -05:00
{
// the keys in $data are the same keys we defined in getOptions()
2020-01-05 16:33:27 -05:00
$newfilename = trim($data['filename']);
2018-12-12 14:36:56 -05:00
// this function should always validate the data.
// in this example we check if file exists
if (!file_exists($newfilename)) {
// in case of error we return an array with
2020-01-05 16:33:27 -05:00
// ['optionname' => 'error message']
return ['filename' => 'The file doesn\'t exists'];
2018-12-12 14:36:56 -05:00
}
2020-01-05 16:33:27 -05:00
$this->config->set('storage', 'samplestorage', $newfilename);
2018-12-12 14:36:56 -05:00
// no errors, return empty array
return [];
}
2020-01-05 16:33:27 -05:00
public function __toString()
{
return self::NAME;
}
2020-01-06 11:42:28 -05:00
public static function getName()
{
return self::NAME;
}
2018-12-12 14:36:56 -05:00
}
```
Now the plugin main file. Here we register and unregister the backend class.
The file is `addon/samplestorage/samplestorage.php`
```php
< ?php
/**
* Name: Sample Storage Addon
* Description: A sample addon which implements an unusefull storage backend
* Version: 1.0.0
* Author: Alice < https: // alice . social /~ alice >
*/
use Friendica\Addon\samplestorage\SampleStorageBackend;
2020-01-04 19:58:49 -05:00
use Friendica\DI;
2018-12-12 14:36:56 -05:00
function samplestorage_install()
{
// on addon install, we register our class with name "Sample Storage".
// note: we use `::class` property, which returns full class name as string
// this save us the problem of correctly escape backslashes in class name
2020-01-08 19:48:48 -05:00
DI::storageManager()->register(SampleStorageBackend::class);
2018-12-12 14:36:56 -05:00
}
function samplestorage_unistall()
{
// when the plugin is uninstalled, we unregister the backend.
2020-01-08 19:48:48 -05:00
DI::storageManager()->unregister(SampleStorageBackend::class);
2018-12-12 14:36:56 -05:00
}
2020-01-08 16:51:37 -05:00
function samplestorage_storage_instance(\Friendica\App $a, array $data)
{
if ($data['name'] === SampleStorageBackend::getName()) {
// instance a new sample storage instance and pass it back to the core for usage
$data['storage'] = new SampleStorageBackend(DI::config(), DI::l10n(), DI::cache());
}
}
2018-12-12 14:52:09 -05:00
```
2018-12-12 14:36:56 -05:00
2020-01-05 16:33:27 -05:00
**Theoretically - until tests for Addons are enabled too - create a test class with the name `addon/tests/SampleStorageTest.php` :
2018-12-12 14:36:56 -05:00
2020-01-05 16:33:27 -05:00
```php
use Friendica\Model\Storage\IStorage;
use Friendica\Test\src\Model\Storage\StorageTest;
2018-12-12 14:36:56 -05:00
2020-01-05 16:33:27 -05:00
class SampleStorageTest extends StorageTest
{
// returns an instance of your newly created storage class
protected function getInstance()
{
// create a new SampleStorageBackend instance with all it's dependencies
// Have a look at DatabaseStorageTest or FilesystemStorageTest for further insights
return new SampleStorageBackend();
}
// Assertion for the option array you return for your new StorageClass
protected function assertOption(IStorage $storage)
{
$this->assertEquals([
'filename' => [
'input',
'The file to return',
'sample.jpg',
'Enter the path to a file'
],
], $storage->getOptions());
}
}
```