Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
### Added
- Access Points (Plugins can define additional access points to restrict user access to certain parts of the plugin)
- Plugin-System now supports custom components, e.g. attribute types
- Plugin-System now supports PluginScopes
- .env variable `ALLOW_FILESYSTEM_MIGRATIONS` to explcitly enable filesystem migrations
- API endpoint for getting all entity details data in one request: GET::v1/entity/{id}/entity_detail
### Fixed
Expand Down
8 changes: 2 additions & 6 deletions app/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Exceptions\AmbiguousValueException;
use App\Import\EntityImporter;
use App\Traits\CommentTrait;
use App\Traits\HasPluginScopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
Expand All @@ -25,6 +26,7 @@ class Entity extends Model implements Searchable {
use CommentTrait;
use SearchableTrait;
use LogsActivity;
use HasPluginScopes;

/**
* The attributes that are assignable.
Expand Down Expand Up @@ -98,12 +100,6 @@ public static function getSearchCols(): array {
return array_keys(self::searchCols);
}

protected static function booted(): void {
$pluginScopes = Plugin::getScopesFor(self::class);
foreach($pluginScopes as $pluginScope) {
static::addGlobalScope(new $pluginScope);
}
}
Comment on lines -101 to -106
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to HasPluginScopes.php then we can add the trait to every model we want in the future.


public static function getFromPath($path, $delimiter = "\\\\"): ?int {
if(!isset($path)) {
Expand Down
110 changes: 61 additions & 49 deletions app/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
Expand Down Expand Up @@ -38,19 +39,19 @@ class Plugin extends Model
'licence',
'title',
];

private static function pluginDirectory() {
$pluginDirectory = config('app.plugin_directory');
return base_path($pluginDirectory);
}

public static function getPluginPath(string $path = ''):string {
if($path === ''){
return self::pluginDirectory();
}
return self::pluginDirectory() . Str::start($path, '/');
}

public static function isInstalled($name): bool {
return self::whereNotNull('installed_at')->where('name', $name)->exists();
}
Expand All @@ -62,7 +63,7 @@ public static function getInstalled(): Collection {
public function slugName(): string {
return Str::slug($this->name);
}

public function getPath(string $path = ''): string {
$pluginPath = $this->name;
if($path !== ''){
Expand Down Expand Up @@ -94,7 +95,7 @@ public static function getPluginInfo($path, $isString = false): mixed {

return json_decode(json_encode($xmlObject), true);
}

public function getInfo(){
return self::getPluginInfo($this->getPath());
}
Expand All @@ -109,7 +110,7 @@ public function getMetadata(): array {
$metadata[$field] = [];
continue;
}

$authors = $info[$field]['author'];
$metadata[$field] = is_array($authors) ? $authors : [$authors];
} else {
Expand Down Expand Up @@ -139,7 +140,7 @@ public function getChangelog(?string $since = null): string {
}

public function getAccessPoints(): array {
$info = self::getInfo(base_path("app/Plugins/$this->name"));
$info = self::getInfo();
$accesspoints = [];
$addedNames = [];
$addedPaths = [];
Expand Down Expand Up @@ -170,51 +171,57 @@ public function getAccessPoints(): array {
return $accesspoints;
}

public function getScopes(): array {
$info = self::getInfo(base_path("app/Plugins/$this->name"), false);
$scopes = [];
if($info !== false) {
if(array_key_exists('scopes', $info)) {
foreach($info['scopes'] as $scope) {
$attributes = $scope['@attributes'];
if(!array_key_exists('src', $attributes)) {
Log::error('<scope> attribute \'src\' is required');
continue;
}
if(!array_key_exists('on', $attributes)) {
Log::error('<scope> attribute \'on\' is required');
continue;
}

$src = $attributes['src'];
$on = $attributes['on'];

$srcDir = base_path("app/Plugins/$this->name/Scopes");
if(!file_exists($srcDir) || !is_dir($srcDir)) {
Log::error('Missing \'Scopes\' directory');
continue;
}
$srcPath = "{$srcDir}/{$src}";
if(!file_exists($srcPath)) {
Log::error("Missing file '$src'");
continue;
}
if(!class_exists($on)) {
Log::error("Class '{$on}' does not exist!");
continue;
}
$className = Str::replaceEnd('.php', '', $src);
$namespacedSrc = "App\Plugins\\$this->name\Scopes\\$className";
private function getScopeCacheKey(): string {
return 'plugin_scopes_' . $this->id;
}

if(!array_key_exists($on, $scopes)) {
$scopes[$on] = [];
public function getScopes(): array {
return Cache::rememberForever($this->getScopeCacheKey(), function() {
Copy link
Contributor Author

@Severino Severino Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the Cache to make access faster without the need to parse the info.xml on every request.
It will be invalidated on install, update, uninstall and remove of the respective plugin.

$info = self::getInfo();
$scopes = [];
if($info !== false) {
if(array_key_exists('scopes', $info)) {
foreach($info['scopes'] as $scope) {
$attributes = $scope['@attributes'];
if(!array_key_exists('src', $attributes)) {
Log::error('<scope> attribute \'src\' is required');
continue;
}
if(!array_key_exists('on', $attributes)) {
Log::error('<scope> attribute \'on\' is required');
continue;
}

$src = $attributes['src'];
$on = $attributes['on'];

$srcDir = $this->getPath("Scopes");
if(!file_exists($srcDir) || !is_dir($srcDir)) {
Log::error('Missing \'Scopes\' directory');
continue;
}
$srcPath = $srcDir . DIRECTORY_SEPARATOR . $src;
if(!file_exists($srcPath)) {
Log::error("Missing file '$src'");
continue;
}
if(!class_exists($on)) {
Log::error("Class '{$on}' does not exist!");
continue;
}
$className = Str::replaceEnd('.php', '', $src);
$namespacedSrc = "App\\Plugins\\$this->name\\Scopes\\$className";

if(!array_key_exists($on, $scopes)) {
$scopes[$on] = [];
}

$scopes[$on][] = $namespacedSrc;
}

$scopes[$on][] = $namespacedSrc;
}
}
}
return $scopes;
return $scopes;
});
}

/**
Expand Down Expand Up @@ -344,11 +351,16 @@ public function updateUpdateState($fromInfoVersion): void {
}
}

public function clearCache(): void {
Cache::forget($this->getScopeCacheKey());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we can add all other plugin Caches, e.g. PluginHooks

}

public function handleInstallation(bool $isUpdate = false): void {
$this->runMigrations();
$this->publishScript();
$this->addPermissions();
$this->installPresetsFromFile();
$this->clearCache();

if(!$isUpdate) {
$this->installed_at = Carbon::now();
Expand All @@ -369,7 +381,7 @@ public function handleUpdate(): string {

public function handleUninstall(): void {
$this->removeScript();

$this->clearCache();
$this->installed_at = null;
$this->save();
}
Expand Down
25 changes: 25 additions & 0 deletions app/Traits/HasPluginScopes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Traits;

use App\Plugin;

trait HasPluginScopes {
/**
* Register plugin scopes for this model
*
* Note: This method is called automatically by Laravel when the model boots.
*/
protected static function bootHasPluginScopes(): void {
// The try-catch is to prevent issues during installation/package discovery
try {
$pluginScopes = Plugin::getScopesFor(static::class);
foreach($pluginScopes as $pluginScope) {
static::addGlobalScope(new $pluginScope);
}
} catch(\Exception $e) {
// Fail silently during installation/package discovery
return;
}
}
}
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@
],
"psr-4": {
"App\\": "app/",
"App\\Plugins\\": "app/Plugins/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
"Tests\\": "tests/",
"App\\Plugins\\": "tests/assets/Plugins/"
Comment on lines +54 to +55
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tests we use this directory. Which was not detected by the auto loader when adding the scopes to the test plugins.
With this the tests should run without problems.

}
},
"extra": {
Expand Down
10 changes: 10 additions & 0 deletions database/seeders/Testing/PluginSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public function run() {
'created_at' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-04-14 04:40:04', 'UTC'),
'updated_at' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-06-16 06:36:27', 'UTC'),
),
2 => array(
'id' => 3,
'name' => 'ScopePlugin',
'version' => '3.2.0',
'uuid' => '123e4567-e89b-12d3-a456-426614174004',
'update_available' => null,
'installed_at' => null,
'created_at' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-08-01 08:00:00', 'UTC'),
'updated_at' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-08-01 08:00:00', 'UTC'),
),
Comment on lines +38 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the scope plugin to have a plugin that provides a scope.

));

// Reset PostgreSQL sequence to continue from the highest ID
Expand Down
Loading
Loading