feat(plugins): register plugins using Plugin.php file instead of namespace + simplify i18n structure

This commit is contained in:
Yassine Doghri 2024-06-03 14:32:42 +00:00
parent d7b9730d7e
commit 2035c39fd1
21 changed files with 224 additions and 88 deletions

View File

@ -96,7 +96,7 @@ class Autoload extends AutoloadConfig
* *
* @var list<string> * @var list<string>
*/ */
public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php']; public $files = [];
/** /**
* ------------------------------------------------------------------- * -------------------------------------------------------------------
@ -110,34 +110,4 @@ class Autoload extends AutoloadConfig
* @var list<string> * @var list<string>
*/ */
public $helpers = ['auth', 'setting', 'icons', 'plugins']; public $helpers = ['auth', 'setting', 'icons', 'plugins'];
public function __construct()
{
// load plugins namespaces
$pluginsPaths = glob(PLUGINS_PATH . '*/*', GLOB_ONLYDIR | GLOB_NOSORT);
if (! $pluginsPaths) {
$pluginsPaths = [];
}
foreach ($pluginsPaths as $pluginPath) {
$vendor = basename(dirname($pluginPath));
$package = basename($pluginPath);
// validate plugin pattern
if (preg_match('~' . PLUGINS_KEY_PATTERN . '~', $vendor . '/' . $package) === false) {
continue;
}
$pluginNamespace = 'Plugins\\' . str_replace(
' ',
'',
ucwords(str_replace(['-', '_', '.'], ' ', $vendor . '\\' . $package))
);
$this->psr4[$pluginNamespace] = $pluginPath;
}
parent::__construct();
}
} }

View File

@ -8,6 +8,8 @@ use CodeIgniter\Config\BaseConfig;
class Plugins extends BaseConfig class Plugins extends BaseConfig
{ {
public string $folder = PLUGINS_PATH;
/** /**
* @var list<string> * @var list<string>
*/ */

View File

@ -15,6 +15,8 @@ class Services extends BaseService
return self::getSharedInstance('plugins'); return self::getSharedInstance('plugins');
} }
return new Plugins(); $config = config('Plugins');
return new Plugins($config);
} }
} }

View File

@ -101,6 +101,11 @@ abstract class BasePlugin implements PluginInterface
return $this->status; return $this->status;
} }
final public function getDirectory(): string
{
return $this->directory;
}
/** /**
* @return array<string,string> * @return array<string,string>
*/ */

View File

@ -8,6 +8,7 @@ use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\SimpleRSSElement;
use Config\Database; use Config\Database;
use Modules\Plugins\Config\Plugins as PluginsConfig;
/** /**
* @method void rssBeforeChannel(Podcast $podcast) * @method void rssBeforeChannel(Podcast $podcast)
@ -63,8 +64,9 @@ class Plugins
protected static int $activeCount = 0; protected static int $activeCount = 0;
public function __construct() public function __construct(
{ protected PluginsConfig $config
) {
helper('plugins'); helper('plugins');
$this->registerPlugins(); $this->registerPlugins();
@ -91,6 +93,14 @@ class Plugins
return array_slice(static::$plugins, (($page - 1) * $perPage), $perPage); return array_slice(static::$plugins, (($page - 1) * $perPage), $perPage);
} }
/**
* @return array<BasePlugin>
*/
public function getAllPlugins(): array
{
return static::$plugins;
}
/** /**
* @return array<BasePlugin> * @return array<BasePlugin>
*/ */
@ -240,8 +250,8 @@ class Plugins
return false; return false;
} }
// delete plugin folder from PLUGINS_PATH // delete plugin folder
$pluginFolder = PLUGINS_PATH . $plugin->getKey(); $pluginFolder = $this->config->folder . $plugin->getKey();
$rmdirResult = $this->rrmdir($pluginFolder); $rmdirResult = $this->rrmdir($pluginFolder);
$transResult = $db->transCommit(); $transResult = $db->transCommit();
@ -252,13 +262,12 @@ class Plugins
protected function registerPlugins(): void protected function registerPlugins(): void
{ {
// search for plugins in plugins folder // search for plugins in plugins folder
$pluginsDirectories = glob(PLUGINS_PATH . '*/*', GLOB_ONLYDIR); $pluginsDirectories = glob($this->config->folder . '*/*', GLOB_ONLYDIR);
if ($pluginsDirectories === false || $pluginsDirectories === []) { if ($pluginsDirectories === false || $pluginsDirectories === []) {
return; return;
} }
$locator = service('locator');
foreach ($pluginsDirectories as $pluginDirectory) { foreach ($pluginsDirectories as $pluginDirectory) {
$vendor = basename(dirname($pluginDirectory)); $vendor = basename(dirname($pluginDirectory));
$package = basename($pluginDirectory); $package = basename($pluginDirectory);
@ -267,13 +276,17 @@ class Plugins
continue; continue;
} }
$pluginFile = $pluginDirectory . DIRECTORY_SEPARATOR . 'Plugin.php'; $className = str_replace(
' ',
'',
ucwords(str_replace(['-', '_', '.'], ' ', $vendor . ' ' . $package)) . 'Plugin'
);
$className = $locator->findQualifiedNameFromPath($pluginFile); spl_autoload_register(static function ($class) use (&$className, &$pluginDirectory): void {
if ($class === $className) {
if ($className === false) { include $pluginDirectory . DIRECTORY_SEPARATOR . 'Plugin.php';
continue; }
} }, true);
$plugin = new $className($vendor, $package, $pluginDirectory); $plugin = new $className($vendor, $package, $pluginDirectory);
if (! $plugin instanceof BasePlugin) { if (! $plugin instanceof BasePlugin) {

View File

@ -49,3 +49,35 @@ if (! function_exists('set_plugin_option')) {
->set($key, $value, $context); ->set($key, $value, $context);
} }
} }
if (! function_exists('load_plugins_translations')) {
/**
* @return array<mixed>
*/
function load_plugins_translations(string $locale): array
{
$allPlugins = plugins()
->getAllPlugins();
$translations = [];
foreach ($allPlugins as $plugin) {
$file = $plugin->getDirectory() . DIRECTORY_SEPARATOR . 'i18n' . DIRECTORY_SEPARATOR . $locale . '.json';
$jsonContents = @file_get_contents($file);
if (! $jsonContents) {
continue;
}
$contents = json_decode($jsonContents, true);
if (! $contents) {
continue;
}
$translations[$plugin->getKey()] = $contents;
}
return $translations;
}
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// get locale from path
$locale = basename(__DIR__);
return load_plugins_translations($locale);

View File

@ -48,17 +48,31 @@ class Field extends ManifestObject
/** /**
* @return array{label:string,value:string,hint:string}[] * @return array{label:string,value:string,hint:string}[]
*/ */
public function getOptionsArray(): array public function getOptionsArray(string $i18nKey): array
{ {
$optionsArray = []; $optionsArray = [];
foreach ($this->options as $option) { foreach ($this->options as $option) {
$optionsArray[] = [ $optionsArray[] = [
'label' => $option->label,
'value' => $option->value, 'value' => $option->value,
'hint' => (string) $option->hint, 'label' => esc($this->getTranslated($i18nKey . '.' . $option->value . '.label', $option->label)),
'hint' => esc($this->getTranslated($i18nKey . '.' . $option->value . '.hint', (string) $option->hint)),
]; ];
} }
return $optionsArray; return $optionsArray;
} }
public function getTranslated(string $i18nKey, string $default): string
{
$key = 'Plugin.' . $i18nKey;
/** @var string $i18nField */
$i18nField = lang($key);
if ($default === '' || $i18nField === $key) {
return $default;
}
return $i18nField;
}
} }

View File

@ -90,11 +90,20 @@ abstract class ManifestObject
if (is_array($cast)) { if (is_array($cast)) {
if (is_array($value)) { if (is_array($value)) {
foreach ($value as $valueKey => $valueElement) { foreach ($value as $valueKey => $valueElement) {
$value[$valueKey] = new $cast[0]($this->pluginKey, $valueElement); if (is_subclass_of($cast[0], self::class)) {
$value[$valueKey] = new $cast[0]($this->pluginKey);
$value[$valueKey]->loadData($valueElement);
} else {
$value[$valueKey] = new $cast[0]($valueElement);
}
} }
} }
} elseif (is_subclass_of($cast, self::class)) {
$valueElement = $value;
$value = new $cast($this->pluginKey);
$value->loadData($valueElement ?? []);
} else { } else {
$value = new $cast($this->pluginKey, $value ?? []); $value = new $cast($value ?? []);
} }
} }

View File

@ -5,24 +5,24 @@
<?php switch ($field->type): case 'checkbox': ?> <?php switch ($field->type): case 'checkbox': ?>
<x-Forms.Checkbox <x-Forms.Checkbox
name="<?= $field->key ?>" name="<?= $field->key ?>"
hint="<?= esc($field->hint) ?>" 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_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
><?= $field->label ?></x-Forms.Checkbox> ><?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?></x-Forms.Checkbox>
<?php break; <?php break;
case 'toggler': ?> case 'toggler': ?>
<x-Forms.Toggler <x-Forms.Toggler
name="<?= $field->key ?>" name="<?= $field->key ?>"
hint="<?= esc($field->hint) ?>" 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_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
><?= $field->label ?></x-Forms.Toggler> ><?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?></x-Forms.Toggler>
<?php break; <?php break;
case 'radio-group': ?> case 'radio-group': ?>
<x-Forms.RadioGroup <x-Forms.RadioGroup
label="<?= $field->label ?>"
name="<?= $field->key ?>" name="<?= $field->key ?>"
options="<?= esc(json_encode($field->getOptionsArray())) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" 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' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -31,10 +31,10 @@
<x-Forms.Field <x-Forms.Field
as="Select" as="Select"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= $field->label ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
options="<?= esc(json_encode($field->getOptionsArray())) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
hint="<?= esc($field->hint) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
helper="<?= esc($field->helper) ?>" options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -43,10 +43,10 @@
<x-Forms.Field <x-Forms.Field
as="SelectMulti" as="SelectMulti"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= $field->label ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
options="<?= esc(json_encode($field->getOptionsArray())) ?>" options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= esc(json_encode(get_plugin_option($plugin->getKey(), $field->key, $context))) ?>" value="<?= esc(json_encode(get_plugin_option($plugin->getKey(), $field->key, $context))) ?>"
/> />
@ -56,9 +56,9 @@
as="Input" as="Input"
type="email" type="email"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -69,9 +69,9 @@
type="url" type="url"
placeholder="https://…" placeholder="https://…"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -81,9 +81,9 @@
as="Input" as="Input"
type="number" type="number"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -92,9 +92,9 @@
<x-Forms.Field <x-Forms.Field
as="Textarea" as="Textarea"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -103,21 +103,21 @@
<x-Forms.Field <x-Forms.Field
as="MarkdownEditor" as="MarkdownEditor"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
<?php break; <?php break;
case 'datetime': ?> case 'datetime':
<?php $hasDatetime = true ?> $hasDatetime = true ?>
<x-Forms.Field <x-Forms.Field
as="DatetimePicker" as="DatetimePicker"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />
@ -126,9 +126,9 @@
<x-Forms.Field <x-Forms.Field
as="Input" as="Input"
name="<?= $field->key ?>" name="<?= $field->key ?>"
label="<?= esc($field->label) ?>" label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
hint="<?= esc($field->hint) ?>" hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
helper="<?= esc($field->helper) ?>" helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
isRequired="<?= $field->optional ? 'false' : 'true' ?>" isRequired="<?= $field->optional ? 'false' : 'true' ?>"
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>" value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/> />

View File

@ -34,6 +34,7 @@ if (isset($episode)) {
'plugin' => $plugin, 'plugin' => $plugin,
'action' => route_to(sprintf('plugins-settings-%s-action', $type), ...$params), 'action' => route_to(sprintf('plugins-settings-%s-action', $type), ...$params),
'fields' => $fields, 'fields' => $fields,
'type' => $type,
'context' => $context, 'context' => $context,
]) ?> ]) ?>
<?= $this->endSection() ?> <?= $this->endSection() ?>