vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
+11
View File
@@ -0,0 +1,11 @@
.DS_Store
/vendor
/.idea
/.vscode
/.vagrant
composer.lock
Homestead.json
Homestead.yaml
yarn-error.log
.phpunit.result.cache
.php_cs.cache
+42
View File
@@ -0,0 +1,42 @@
{
"name": "vanguardapp/plugins",
"description": "Core stuff for Vanguard plugin support.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Milos Stojanovic",
"email": "stojanovic.loshmi@gmail.com",
"homepage": "https://mstojanovic.net",
"role": "Developer"
}
],
"require": {
"illuminate/support": "^10.0|^11.0",
"symfony/process": "^6.0|^7.0",
"illuminate/console": "^10.0|^11.0",
"illuminate/filesystem": "^10.0|^11.0",
"illuminate/view": "^10.0|^11.0",
"illuminate/auth": "^10.0|^11.0"
},
"require-dev": {
"laravel/pint": "^1.13"
},
"autoload": {
"psr-4": {
"Vanguard\\Plugins\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Vanguard\\Plugins\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Vanguard\\Plugins\\PluginsServiceProvider"
]
}
}
}
+13
View File
@@ -0,0 +1,13 @@
Core classes for [Vanguard](https://vanguardapp.io) plugin support.
## Installation
To install the package just run the following command:
```
composer require vanguardapp/plugins
```
## License
This plugin is an open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT).
@@ -0,0 +1,189 @@
<?php
namespace Vanguard\Plugins\Console\Commands;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class GeneratePluginCommand extends PluginCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'vanguard:make-plugin';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Scaffold all the necessary files for the new plugin.';
/**
* Execute the console command.
*
* @throws FileNotFoundException
*/
public function handle()
{
if ($this->pluginExists($this->pluginPath())) {
$this->error('Plugin already exists!');
return;
}
$this->warn('Generating plugin folder structure...');
$this->generateFolderStructure();
$this->generateFiles();
$this->info('Plugin folder structure generated successfully.');
$this->warn('Installing plugin as a local composer dependency...');
$this->updateApplicationComposerFile();
$this->installPlugin();
$this->info('Plugin installed successfully.');
}
/**
* Generate the plugin folder structure.
*/
private function generateFolderStructure(): void
{
$pluginPath = $this->pluginPath();
foreach ($this->getFolders() as $folder) {
$this->makeDirectory($pluginPath."/{$folder}");
}
}
/**
* Get the list of folders to create.
*/
protected function getFolders(): array
{
return [
'database/factories',
'database/migrations',
'database/seeds',
'resources/views',
'routes',
'src/Http/Controllers/Api',
'src/Http/Controllers/Web',
'src/Http/Requests',
'tests',
];
}
/**
* Generate necessary plugin files.
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
private function generateFiles(): void
{
$pluginPath = $this->pluginPath();
foreach ($this->getFiles() as $stubPath => $filePath) {
$this->makeDirectory($pluginPath."/{$filePath}");
$stub = $this->files->get(__DIR__.'/stubs/'.$stubPath);
$stub = $this->replacePlaceholders($stub);
$this->files->put($pluginPath."/{$filePath}", $stub);
}
}
/**
* Get the list of files that should be created for the plugin.
*/
protected function getFiles(): array
{
return [
'routes/web.stub' => 'routes/web.php',
'routes/api.stub' => 'routes/api.php',
'views/index.stub' => 'resources/views/index.blade.php',
'config.stub' => 'config/config.php',
'composer.stub' => 'composer.json',
'assets/js/app.stub' => 'resources/assets/js/app.js',
'assets/sass/app.stub' => 'resources/assets/sass/app.scss',
'webpack.stub' => 'webpack.mix.js',
'package.stub' => 'package.json',
'unit-test.stub' => 'tests/Unit/ExampleTest.php',
'feature-test.stub' => 'tests/Feature/FeatureTest.php',
'gitignore.stub' => '.gitignore',
'plugin.stub' => 'src/'.$this->studlyName().'.php',
'controller.stub' => 'src/Http/Controllers/Web/'.$this->studlyName().'Controller.php',
];
}
/**
* Replace placeholders within the stub files.
*
* @return mixed
*/
private function replacePlaceholders(string $stub)
{
return str_replace(
[
'$ROOT_NAMESPACE$',
'$PLUGIN_NAMESPACE$',
'$VENDOR$',
'$AUTHOR_NAME$',
'$AUTHOR_EMAIL$',
'$SNAKE_NAME$',
'$STUDLY_NAME$',
],
[
$this->rootNamespace(),
$this->pluginNamespace(),
config('plugins.composer.vendor'),
config('plugins.composer.author.name'),
config('plugins.composer.author.email'),
$this->snakeName(),
$this->studlyName(),
],
$stub
);
}
/**
* Update the main application composer file to include the
* newly generated plugin.
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
private function updateApplicationComposerFile(): void
{
$composer = json_decode($this->files->get(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './plugins/'.$this->studlyName(),
];
$this->files->put(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the plugin via composer.
*/
private function installPlugin(): void
{
$pluginFullName = sprintf('%s/%s', config('plugins.composer.vendor'), $this->snakeName());
$command = Process::fromShellCommandline("composer require {$pluginFullName} \"*\"");
$command->setWorkingDirectory(base_path());
$command->run();
if (! $command->isSuccessful()) {
throw new ProcessFailedException($command);
}
}
}
@@ -0,0 +1,105 @@
<?php
namespace Vanguard\Plugins\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputArgument;
abstract class PluginCommand extends Command
{
/**
* Create a new controller creator command instance.
*
* @return void
*/
public function __construct(protected Filesystem $files)
{
parent::__construct();
}
/**
* Get the console command arguments.
*/
protected function getArguments(): array
{
return [
['name', InputArgument::REQUIRED, 'The name of the plugin.'],
];
}
/**
* Get the desired class name from the input.
*/
protected function getNameInput(): string
{
return trim($this->argument('name'));
}
/**
* Get the root namespace for the class.
*/
protected function rootNamespace(): string
{
return rtrim($this->laravel->getNamespace(), '\\');
}
/**
* The namespace of the plugin itself.
*
* @return string
*/
protected function pluginNamespace()
{
return sprintf("%s\%s", $this->rootNamespace(), $this->studlyName());
}
/**
* Build the directory for the class if necessary.
*/
protected function makeDirectory(string $path): string
{
if (! $this->files->isDirectory(dirname($path))) {
$this->files->makeDirectory(dirname($path), 0777, true, true);
}
return $path;
}
/**
* The path to the plugin directory.
*/
protected function pluginPath(): string
{
$name = Str::replaceFirst($this->rootNamespace(), '', $this->getNameInput());
return $this->laravel['path.base'].'/plugins/'.str_replace('\\', '/', Str::studly($name));
}
/**
* Check if plugin exists on a given path.
*
* @param string $pluginPath
*/
protected function pluginExists($pluginPath): bool
{
return $this->files->exists($pluginPath);
}
/**
* Name of the plugin in StudlyCase format.
*/
protected function studlyName(): string
{
return Str::studly($this->getNameInput());
}
/**
* Name of the plugin in snake-case format.
*/
protected function snakeName(): string
{
return Str::snake($this->getNameInput(), '-');
}
}
@@ -0,0 +1,102 @@
<?php
namespace Vanguard\Plugins\Console\Commands;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Vanguard\Plugins\Vanguard;
class RemovePluginCommand extends PluginCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'vanguard:remove-plugin';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes the specified plugin from the system.';
/**
* Execute the console command.
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function handle(): void
{
if (! $this->pluginExists($this->pluginPath())) {
$this->error("The plugin [{$this->studlyName()}] does not exist.");
return;
}
if ($this->pluginIsStillActive()) {
$message = "The [{$this->studlyName()}] plugin is still active. Please remove it from the list of active";
$message .= ' plugins inside the VanguardServiceProvider first.';
$this->error($message);
return;
}
$this->warn("Removing [{$this->studlyName()}] plugin...");
$this->files->deleteDirectory($this->pluginPath());
$this->updateApplicationComposerFile();
$this->uninstallPluginDependency();
$this->info('Plugin removed successfully.');
}
/**
* Check if the plugin that should be deleted is still active.
*/
private function pluginIsStillActive(): bool
{
$pluginClassName = sprintf(
"%s\%s",
$this->pluginNamespace(),
$this->studlyName()
);
return isset(Vanguard::availablePlugins()[$pluginClassName]);
}
/**
* Uninstall plugin composer dependency.
*/
private function uninstallPluginDependency(): void
{
$pluginFullName = sprintf('%s/%s', config('plugins.composer.vendor'), $this->snakeName());
$command = Process::fromShellCommandline("composer remove {$pluginFullName}");
$command->setWorkingDirectory(base_path());
$command->run();
if (! $command->isSuccessful()) {
throw new ProcessFailedException($command);
}
}
/**
* Update the main application composer file and remove
* the reference to the plugin.
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
private function updateApplicationComposerFile(): void
{
$composer = json_decode($this->files->get(base_path('composer.json')), true);
$composer['repositories'] = collect($composer['repositories'])->filter(function ($repo) {
return ! isset($repo['url']) || $repo['url'] != './plugins/'.$this->studlyName();
})->toArray();
$this->files->put(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
}
@@ -0,0 +1,20 @@
{
"name": "$VENDOR$/$SNAKE_NAME$",
"description": "",
"authors": [
{
"name": "$AUTHOR_NAME$",
"email": "$AUTHOR_EMAIL$"
}
],
"autoload": {
"psr-4": {
"$ROOT_NAMESPACE$\\$STUDLY_NAME$\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"$ROOT_NAMESPACE$\\$STUDLY_NAME$\\Tests\\": "tests/"
}
}
}
@@ -0,0 +1,5 @@
<?php
return [
//
];
@@ -0,0 +1,18 @@
<?php
namespace $PLUGIN_NAMESPACE$\Http\Controllers\Web;
use Vanguard\Http\Controllers\Controller;
class $STUDLY_NAME$Controller extends Controller
{
/**
* Displays the plugin index page.
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
return view('$SNAKE_NAME$::index');
}
}
@@ -0,0 +1,20 @@
<?php
namespace $PLUGIN_NAMESPACE$\Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
@@ -0,0 +1,11 @@
/dist
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db
@@ -0,0 +1,16 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.1.4",
"laravel-mix": "^2.0"
}
}
@@ -0,0 +1,125 @@
<?php
namespace $PLUGIN_NAMESPACE$;
use Route;
use Illuminate\Database\Eloquent\Factory;
use Vanguard\Plugins\Plugin;
use Vanguard\Support\Sidebar\Item;
class $STUDLY_NAME$ extends Plugin
{
/**
* A sidebar item for the plugin.
*/
public function sidebar(): ?Item
{
return null;
}
/**
* Register plugin services required.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*
* @return void
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function boot(): void
{
$this->registerConfig();
$this->registerViews();
$this->registerFactories();
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->loadViewsFrom(__DIR__ . '/../resources/views', '$SNAKE_NAME$');
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->mapRoutes();
$this->registerFactories();
}
/**
* Register plugin configuration files.
*/
protected function registerConfig(): void
{
$configPath = __DIR__.'/../config/config.php';
$this->publishes([$configPath => config_path('$SNAKE_NAME$.php')], 'config');
$this->mergeConfigFrom($configPath, '$SNAKE_NAME$');
}
/**
* Register plugin views.
*
* @return void
*/
protected function registerViews(): void
{
$viewsPath = __DIR__.'/../resources/views';
$this->publishes([
$viewsPath => resource_path('views/plugins/$SNAKE_NAME$')
], 'views');
$this->loadViewsFrom($viewsPath, '$SNAKE_NAME$');
}
/**
* Map all plugin related routes.
*/
protected function mapRoutes(): void
{
$this->mapWebRoutes();
if ($this->app['config']->get('auth.expose_api')) {
$this->mapApiRoutes();
}
}
/**
* Map web plugin related routes.
*/
protected function mapWebRoutes(): void
{
Route::group([
'namespace' => '$PLUGIN_NAMESPACE$\Http\Controllers\Web',
'middleware' => 'web',
], function () {
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
});
}
/**
* Map API plugin related routes.
*/
protected function mapApiRoutes(): void
{
Route::group([
'namespace' => '$PLUGIN_NAMESPACE$\Http\Controllers\Api',
'middleware' => 'api',
'prefix' => 'api',
], function () {
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
});
}
/**
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
private function registerFactories(): void
{
if (! $this->app->environment('production') && $this->app->runningInConsole()) {
$this->app->make(Factory::class)->load(__DIR__ . '/../database/factories');
}
}
}
@@ -0,0 +1,17 @@
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application.
| Enjoy building your API!
|
*/
Route::get('/$SNAKE_NAME$', function (Request $request) {
return $request->user();
});
@@ -0,0 +1,15 @@
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application.
| Now create something great!
|
*/
Route::prefix('$SNAKE_NAME$')->group(function () {
Route::get('/', '$STUDLY_NAME$Controller@index')->middleware('auth');
});
@@ -0,0 +1,20 @@
<?php
namespace $PLUGIN_NAMESPACE$\Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
/**
* A basic unit test example.
*
* @return void
*/
public function testExample()
{
$this->assertTrue(true);
}
}
@@ -0,0 +1,16 @@
@extends('layouts.app')
@section('page-title', '$STUDLY_NAME$')
@section('page-heading', '$STUDLY_NAME$')
@section('breadcrumbs')
<li class="breadcrumb-item active">
$STUDLY_NAME$
</li>
@stop
@section('content')
<p>
This view is loaded from <strong>$STUDLY_NAME$</strong> plugin.
</p>
@stop
@@ -0,0 +1,10 @@
const mix = require('laravel-mix');
mix.setPublicPath('./dist/');
mix.js( __dirname + '/resources/assets/js/app.js', 'dist/js/announcements.js')
.sass( __dirname + '/resources/assets/sass/app.scss', 'dist/css/announcements.css');
if (mix.inProduction()) {
mix.version();
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Vanguard\Plugins\Contracts;
use Illuminate\Contracts\View\View;
interface Hook
{
/**
* Execute the hook action.
*/
public function handle(): View;
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Vanguard\Plugins;
use Illuminate\Support\ServiceProvider;
use Vanguard\Support\Sidebar\Item;
abstract class Plugin extends ServiceProvider
{
/**
* A sidebar item for the plugin.
*
* @return mixed|null
*/
public function sidebar(): ?Item
{
return null;
}
/**
* Boot all the necessary plugin stuff. Basically it will
* work as a plugin service provider that should ensure that all the
* necessary plugin stuff is loaded, so it can work properly.
*/
public function boot(): void
{
}
}
@@ -0,0 +1,35 @@
<?php
namespace Vanguard\Plugins;
use Illuminate\Support\ServiceProvider;
use Vanguard\Plugins\Console\Commands\GeneratePluginCommand;
use Vanguard\Plugins\Console\Commands\RemovePluginCommand;
class PluginsServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->commands([
GeneratePluginCommand::class,
RemovePluginCommand::class,
]);
}
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace Vanguard\Plugins;
use Illuminate\Contracts\Auth\Authenticatable;
class Vanguard
{
/**
* All of the registered Vanguard plugins.
*
* @var array
*/
public static $plugins = [];
/**
* All of the registered Vanguard dashboard widgets.
*
* @var array
*/
public static $widgets = [];
/**
* All of the registered Vanguard scripts.
*
* @var array
*/
public static $scripts = [];
/**
* All of the registered Vanguard styles.
*
* @var array
*/
public static $styles = [];
/**
* All registered Vanguard view hooks.
*
* @var array
*/
public static $hooks = [];
/**
* Register a new view hook.
*/
public static function hook($name, $handler)
{
self::$hooks[$name][] = $handler;
}
/**
* Check if there are handlers registered for the
* provided hook name.
*
* @return bool
*/
public static function hasHook($name)
{
return isset(self::$hooks[$name]);
}
/**
* Get all handlers for a given hook name.
*
* @return mixed
*/
public static function getHookHandlers($name)
{
return data_get(self::$hooks, $name);
}
/**
* Register the given plugins.
*/
public static function plugins(array $plugins)
{
self::$plugins = array_merge(self::$plugins, $plugins);
}
/**
* Get the list of registered plugins.
*
* @return array
*/
public static function availablePlugins()
{
return self::$plugins;
}
/**
* Register the list of given dashboard widgets.
*/
public static function widgets(array $widgets)
{
self::$widgets = array_merge(self::$widgets, $widgets);
}
/**
* Get the list of widgets available for the provided user.
*
* @return array
*/
public static function availableWidgets(Authenticatable $user)
{
return collect(self::$widgets)->filter->authorize($user)->values();
}
}
@@ -0,0 +1,53 @@
<?php
namespace Vanguard\Plugins;
use Illuminate\Support\ServiceProvider;
abstract class VanguardServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$instances = [];
foreach ($this->plugins() as $plugin) {
$instances[$plugin] = $this->app->register($plugin);
}
Vanguard::plugins($instances);
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$widgets = collect($this->widgets())->map(function ($class) {
return $this->app->make($class);
})->toArray();
Vanguard::widgets($widgets);
\Blade::directive('hook', function ($name) {
return "<?php if (\Vanguard\Plugins\Vanguard::hasHook($name)) {
collect(\Vanguard\Plugins\Vanguard::getHookHandlers($name))
->each(function (\$hook) {
echo resolve(\$hook)->handle();
});
} ?>";
});
}
/**
* Dashboard widgets.
*/
abstract protected function widgets(): array;
/**
* List of registered plugins.
*/
abstract protected function plugins(): array;
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace Vanguard\Plugins;
use Closure;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\View\View;
abstract class Widget
{
/**
* The number of columns that widget should take on the dashboard.
* Possible values are from 1 to 12. Set the width to NULL
* if you don't want to disable the width class.
*/
public ?string $width = null;
/**
* Permissions required for viewing the widget.
*/
protected string|array|Closure $permissions;
/**
* Renders the widget HTML.
*/
abstract public function render(): View;
/**
* Authorize the request to verify if a user should be able to
* see the widget on the dashboard.
*
* @return bool
*/
public function authorize(Authenticatable $user)
{
if ($this->permissions instanceof Closure) {
return call_user_func($this->permissions, $user);
}
foreach ((array) $this->permissions as $permission) {
if (! $user->hasPermission($permission)) {
return false;
}
}
return true;
}
/**
* Set permissions required for viewing the widget on the dashboard.
*/
public function permissions($permissions): self
{
$this->permissions = $permissions;
return $this;
}
/**
* Custom scripts that are required by this widget to work
* and that should be rendered on the dashboard only.
*/
public function scripts(): ?View
{
return null;
}
}