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>
*/
public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php'];
public $files = [];
/**
* -------------------------------------------------------------------
@ -110,34 +110,4 @@ class Autoload extends AutoloadConfig
* @var list<string>
*/
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
{
public string $folder = PLUGINS_PATH;
/**
* @var list<string>
*/

View File

@ -15,6 +15,8 @@ class Services extends BaseService
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;
}
final public function getDirectory(): string
{
return $this->directory;
}
/**
* @return array<string,string>
*/

View File

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

View File

@ -49,3 +49,35 @@ if (! function_exists('set_plugin_option')) {
->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}[]
*/
public function getOptionsArray(): array
public function getOptionsArray(string $i18nKey): array
{
$optionsArray = [];
foreach ($this->options as $option) {
$optionsArray[] = [
'label' => $option->label,
'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;
}
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($value)) {
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 {
$value = new $cast($this->pluginKey, $value ?? []);
$value = new $cast($value ?? []);
}
}

View File

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

View File

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