test(plugins): add test suite for Plugins service

This commit is contained in:
Yassine Doghri 2024-06-04 13:10:56 +00:00
parent 3a900bbab6
commit 91dc8c8325
29 changed files with 416 additions and 59 deletions

View File

@ -52,11 +52,7 @@ abstract class BasePlugin implements PluginInterface
$this->manifest = new Manifest($this->key);
$this->manifest->loadFromFile($manifestPath);
if ($this->getManifestErrors() !== []) {
$this->status = PluginStatus::INVALID;
} else {
$this->status = get_plugin_option($this->key, 'active') ? PluginStatus::ACTIVE : PluginStatus::INACTIVE;
}
$this->status = get_plugin_setting($this->key, 'active') ? PluginStatus::ACTIVE : PluginStatus::INACTIVE;
$this->iconSrc = $this->loadIcon($directory . '/icon.svg');
@ -98,17 +94,48 @@ abstract class BasePlugin implements PluginInterface
final public function getGeneralSetting(string $key): mixed
{
return get_plugin_option($this->key, $key);
return get_plugin_setting($this->key, $key);
}
final public function getPodcastSetting(int $podcastId, string $key): mixed
{
return get_plugin_option($this->key, $key, ['podcast', $podcastId]);
return get_plugin_setting($this->key, $key, ['podcast', $podcastId]);
}
final public function getEpisodeSetting(int $episodeId, string $key): mixed
{
return get_plugin_option($this->key, $key, ['episode', $episodeId]);
return get_plugin_setting($this->key, $key, ['episode', $episodeId]);
}
/**
* @return bool true on success, false on failure
*/
final public function activate(): bool
{
if ($this->status === PluginStatus::ACTIVE) {
return false;
}
$this->setStatus(PluginStatus::ACTIVE);
if ($this->status === PluginStatus::INACTIVE) {
return false;
}
set_plugin_setting($this->key, 'active', true);
return true;
}
final public function deactivate(): bool
{
if ($this->status !== PluginStatus::ACTIVE) {
return false;
}
$this->setStatus(PluginStatus::INACTIVE);
set_plugin_setting($this->key, 'active', false);
return true;
}
final public function getStatus(): PluginStatus
@ -243,14 +270,30 @@ abstract class BasePlugin implements PluginInterface
return $this->manifest->license ?? 'UNLICENSED';
}
/**
* @param PluginStatus::ACTIVE|PluginStatus::INACTIVE $value
*/
final protected function setStatus(PluginStatus $value): self
{
if ($this->getManifestErrors() !== []) {
$this->status = PluginStatus::INVALID;
return $this;
}
$this->status = $value;
return $this;
}
final protected function getOption(string $option): mixed
{
return get_plugin_option($this->key, $option);
return get_plugin_setting($this->key, $option);
}
final protected function setOption(string $option, mixed $value = null): void
{
set_plugin_option($this->key, $option, $value);
set_plugin_setting($this->key, $option, $value);
}
private function loadIcon(string $path): string

View File

@ -209,12 +209,16 @@ class Plugins
public function activate(BasePlugin $plugin): void
{
set_plugin_option($plugin->getKey(), 'active', true);
if ($plugin->activate()) {
++self::$activeCount;
}
}
public function deactivate(BasePlugin $plugin): void
{
set_plugin_option($plugin->getKey(), 'active', false);
if ($plugin->deactivate()) {
--self::$activeCount;
}
}
/**
@ -222,7 +226,7 @@ class Plugins
*/
public function setOption(BasePlugin $plugin, string $name, mixed $value, array $additionalContext = null): void
{
set_plugin_option($plugin->getKey(), $name, $value, $additionalContext);
set_plugin_setting($plugin->getKey(), $name, $value, $additionalContext);
}
public function getInstalledCount(): int
@ -282,12 +286,23 @@ class Plugins
ucwords(str_replace(['-', '_', '.'], ' ', $vendor . ' ' . $package)) . 'Plugin'
);
spl_autoload_register(static function ($class) use (&$className, &$pluginDirectory): void {
if ($class === $className) {
include $pluginDirectory . DIRECTORY_SEPARATOR . 'Plugin.php';
$pluginFile = $pluginDirectory . DIRECTORY_SEPARATOR . 'Plugin.php';
spl_autoload_register(static function ($class) use (&$className, &$pluginFile): void {
if ($class !== $className) {
return;
}
if (! file_exists($pluginFile)) {
return;
}
include_once $pluginFile;
}, true);
if (! class_exists($className)) {
continue;
}
$plugin = new $className($vendor, $package, $pluginDirectory);
if (! $plugin instanceof BasePlugin) {
continue;

View File

@ -11,11 +11,11 @@ if (! function_exists('plugins')) {
}
}
if (! function_exists('get_plugin_option')) {
if (! function_exists('get_plugin_setting')) {
/**
* @param ?array{'podcast'|'episode',int} $additionalContext
*/
function get_plugin_option(string $pluginKey, string $option, array $additionalContext = null): mixed
function get_plugin_setting(string $pluginKey, string $option, array $additionalContext = null): mixed
{
$key = sprintf('Plugins.%s', $option);
$context = sprintf('plugin:%s', $pluginKey);
@ -28,11 +28,11 @@ if (! function_exists('get_plugin_option')) {
}
}
if (! function_exists('set_plugin_option')) {
if (! function_exists('set_plugin_setting')) {
/**
* @param ?array{'podcast'|'episode',int} $additionalContext
*/
function set_plugin_option(
function set_plugin_setting(
string $pluginKey,
string $option,
mixed $value = null,

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\Modules\Api\Rest\V1;
namespace Tests\Modules\Plugins;
use CodeIgniter\Test\CIUnitTestCase;
use Modules\Plugins\Manifest\Manifest;
@ -20,7 +20,7 @@ final class ManifestTest extends CIUnitTestCase
$this->assertNotEquals($manifest->name, 'acme/hello-world');
$this->assertNotEquals($manifest->version, '1.0.0');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-required.json');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/mocks/manifests/manifest-required.json');
// no errors
$this->assertEmpty($manifest->getPluginErrors('acme/hello-world'));
@ -34,7 +34,7 @@ final class ManifestTest extends CIUnitTestCase
{
$manifest = new Manifest('acme/hello-world');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-empty.json');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/mocks/manifests/manifest-empty.json');
$errors = $manifest->getPluginErrors('acme/hello-world');
@ -49,7 +49,7 @@ final class ManifestTest extends CIUnitTestCase
{
$manifest = new Manifest('acme/hello-world');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-full-valid.json');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/mocks/manifests/manifest-full-valid.json');
// no errors
$this->assertEmpty($manifest->getPluginErrors('acme/hello-world'));
@ -59,7 +59,7 @@ final class ManifestTest extends CIUnitTestCase
{
$manifest = new Manifest('acme/hello-world');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/Mocks/manifests/manifest-full-invalid.json');
$manifest->loadFromFile(TESTPATH . 'modules/Plugins/mocks/manifests/manifest-full-invalid.json');
// errors
$this->assertNotEmpty($manifest->getPluginErrors('acme/hello-world'));

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Modules\Plugins\Mocks\Plugins\AcmeHelloUniverse;
use Modules\Plugins\Core\BasePlugin;
class Plugin extends BasePlugin
{
}

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Modules\Plugins\Mocks\Plugins\AcmeHelloWorld;
use Modules\Plugins\Core\BasePlugin;
class Plugin extends BasePlugin
{
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Tests\Modules\Plugins;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Modules\Plugins\Config\Plugins as PluginsConfig;
use Modules\Plugins\Core\Plugins;
use Modules\Plugins\Core\PluginStatus;
use Override;
/**
* @internal
*/
final class PluginsTest extends CIUnitTestCase
{
use DatabaseTestTrait;
protected $migrate = true;
protected $migrateOnce = false;
protected $refresh = true;
/**
* @var string|null
*/
protected $namespace;
protected static Plugins $plugins;
#[Override]
public static function setUpBeforeClass(): void
{
$pluginsConfig = new PluginsConfig();
$pluginsConfig->folder = __DIR__ . '/mocks/plugins' . DIRECTORY_SEPARATOR;
self::$plugins = new Plugins($pluginsConfig);
}
public function testRegister(): void
{
$this->assertCount(7, self::$plugins->getAllPlugins());
$this->assertEquals(7, self::$plugins->getInstalledCount());
$this->assertEquals(0, self::$plugins->getActiveCount());
}
public function testActivateDeactivate(): void
{
$this->assertEquals(0, self::$plugins->getActiveCount());
$plugin = self::$plugins->getAllPlugins()[0];
// get first plugin and activate it
self::$plugins->activate($plugin);
$this->assertEquals(1, self::$plugins->getActiveCount());
$this->assertEquals(PluginStatus::ACTIVE, $plugin->getStatus());
$this->seeInDatabase('settings', [
'class' => PluginsConfig::class,
'key' => 'active',
'value' => '1',
'type' => 'boolean',
'context' => 'plugin:' . $plugin->getKey(),
]);
// get first plugin and deactivate it
self::$plugins->deactivate($plugin);
$this->assertEquals(0, self::$plugins->getActiveCount());
$this->assertEquals(PluginStatus::INACTIVE, $plugin->getStatus());
$this->seeInDatabase('settings', [
'class' => PluginsConfig::class,
'key' => 'active',
'value' => '0',
'type' => 'boolean',
'context' => 'plugin:' . $plugin->getKey(),
]);
}
public function testRunHooksActive(): void
{
$acmeAllHooksPlugin = self::$plugins->getPlugin('acme', 'all-hooks');
self::$plugins->activate($acmeAllHooksPlugin);
$this->assertEquals(1, self::$plugins->getActiveCount());
$podcast = new Podcast();
$this->assertEquals('', $podcast->title);
self::$plugins->runHook('rssBeforeChannel', [$podcast]);
$this->assertEquals('Podcast test', $podcast->title);
$channel = new SimpleRSSElement('<channel></channel>');
$this->assertTrue(empty($channel->foo));
self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]);
$this->assertFalse(empty($channel->foo));
$episode = new Episode();
$this->assertEquals('', $episode->title);
self::$plugins->runHook('rssBeforeItem', [$episode]);
$this->assertEquals('Episode test', $episode->title);
$item = new SimpleRSSElement('<item></item>');
$this->assertTrue(empty($item->efoo));
self::$plugins->runHook('rssAfterItem', [$episode, $item]);
$this->assertFalse(empty($item->efoo));
ob_start();
self::$plugins->runHook('siteHead', []);
$result = ob_get_contents();
ob_end_clean(); //Discard output buffer
$this->assertEquals('hello', $result);
}
public function testRunHooksInactive(): void
{
$acmeAllHooksPlugin = self::$plugins->getPlugin('acme', 'all-hooks');
self::$plugins->deactivate($acmeAllHooksPlugin);
$this->assertEquals(0, self::$plugins->getActiveCount());
// nothing should change when running hooks as the plugin is inactive
$podcast = new Podcast();
$this->assertEquals('', $podcast->title);
self::$plugins->runHook('rssBeforeChannel', [$podcast]);
$this->assertEquals('', $podcast->title);
$channel = new SimpleRSSElement('<channel></channel>');
$this->assertTrue(empty($channel->foo));
self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]);
$this->assertTrue(empty($channel->foo));
$episode = new Episode();
$this->assertEquals('', $episode->title);
self::$plugins->runHook('rssBeforeItem', [$episode]);
$this->assertEquals('', $episode->title);
$item = new SimpleRSSElement('<item></item>');
$this->assertTrue(empty($item->efoo));
self::$plugins->runHook('rssAfterItem', [$episode, $item]);
$this->assertTrue(empty($item->efoo));
ob_start();
self::$plugins->runHook('siteHead', []);
$result = ob_get_contents();
ob_end_clean(); //Discard output buffer
$this->assertEquals('', $result);
}
public function testRunUndeclaredHook(): void
{
$acmeUndeclaredHookPlugin = self::$plugins->getPlugin('acme', 'undeclared-hook');
self::$plugins->activate($acmeUndeclaredHookPlugin);
$podcast = new Podcast();
$this->assertEquals('', $podcast->title);
self::$plugins->runHook('rssBeforeChannel', [$podcast]);
$this->assertEquals('Podcast test undeclared', $podcast->title);
// rssAfterChannel has not been declared in plugin manifest, should not be running
$channel = new SimpleRSSElement('<channel></channel>');
$this->assertTrue(empty($channel->foo));
self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]);
$this->assertTrue(empty($channel->foo));
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use Modules\Plugins\Core\BasePlugin;
class AcmeAllHooksPlugin extends BasePlugin
{
#[Override]
public function rssBeforeChannel(Podcast $podcast): void
{
$podcast->title = 'Podcast test';
}
#[Override]
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void
{
$channel->addChild('foo', 'bar');
}
#[Override]
public function rssBeforeItem(Episode $episode): void
{
$episode->title = 'Episode test';
}
#[Override]
public function rssAfterItem(Episode $episode, SimpleRSSElement $item): void
{
$item->addChild('efoo', 'ebar');
}
#[Override]
public function siteHead(): void
{
echo 'hello';
}
}

View File

@ -0,0 +1,11 @@
{
"name": "acme/all-hooks",
"version": "1.0.0",
"hooks": [
"rssBeforeChannel",
"rssAfterChannel",
"rssBeforeItem",
"rssAfterItem",
"siteHead"
]
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Modules\Plugins\Core\BasePlugin;
class AcmeEmptyManifestPlugin extends BasePlugin
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Modules\Plugins\Core\BasePlugin;
class AcmeHelloUniversePlugin extends BasePlugin
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Modules\Plugins\Core\BasePlugin;
class AcmeHelloWorldPlugin extends BasePlugin
{
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use Modules\Plugins\Core\BasePlugin;
class AcmeUndeclaredHookPlugin extends BasePlugin
{
#[Override]
public function rssBeforeChannel(Podcast $podcast): void
{
$podcast->title = 'Podcast test undeclared';
}
#[Override]
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void
{
$channel->addChild('foo', 'bar');
}
}

View File

@ -0,0 +1,5 @@
{
"name": "acme/all-hooks",
"version": "1.0.0",
"hooks": ["rssBeforeChannel"]
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Modules\Plugins\Core\BasePlugin;
class AtlantisHelloBroken extends BasePlugin
{
}

View File

@ -0,0 +1,4 @@
{
"name": "atlantis/hello-broken",
"version": "1.0.0"
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Modules\Plugins\Core\BasePlugin;
class AtlantisHelloWorldPlugin extends BasePlugin
{
}

View File

@ -0,0 +1,4 @@
{
"name": "atlantis/hello-world",
"version": "1.0.0"
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Modules\Plugins\Core\BasePlugin;
class AtlantisNoManifestPlugin extends BasePlugin
{
}

View File

@ -0,0 +1,4 @@
{
"name": "atlantis/no-plugin-file",
"version": "1.0.0"
}

View File

@ -6,14 +6,14 @@
<x-Forms.Checkbox
name="<?= $field->key ?>"
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
isChecked="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
isChecked="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
><?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?></x-Forms.Checkbox>
<?php break;
case 'toggler': ?>
<x-Forms.Toggler
name="<?= $field->key ?>"
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
isChecked="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
isChecked="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
><?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?></x-Forms.Toggler>
<?php break;
case 'radio-group': ?>
@ -24,7 +24,7 @@
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'select': ?>
@ -36,7 +36,7 @@
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'select-multiple': ?>
@ -48,7 +48,7 @@
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= esc(json_encode(get_plugin_option($plugin->getKey(), $field->key, $context))) ?>"
value="<?= esc(json_encode(get_plugin_setting($plugin->getKey(), $field->key, $context))) ?>"
/>
<?php break;
case 'email': ?>
@ -60,7 +60,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'url': ?>
@ -73,7 +73,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'number': ?>
@ -85,7 +85,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'textarea': ?>
@ -96,7 +96,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'markdown': ?>
@ -107,7 +107,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
case 'datetime':
@ -119,7 +119,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php break;
default: ?>
@ -130,7 +130,7 @@
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
/>
<?php endswitch; ?>