First commit
This commit is contained in:
commit
7cf3558ba7
12933 changed files with 1180047 additions and 0 deletions
56
packages/flux-cms/core/composer.json
Normal file
56
packages/flux-cms/core/composer.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "flux-cms/core",
|
||||
"description": "Flux CMS Core Package - Multi-domain, component-first CMS for Laravel",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"cms",
|
||||
"livewire",
|
||||
"multi-domain",
|
||||
"components"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Flux CMS Contributors",
|
||||
"email": "contributors@flux-cms.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^11.0|^12.0",
|
||||
"spatie/laravel-translatable": "^6.0",
|
||||
"spatie/laravel-medialibrary": "^11.0",
|
||||
"spatie/laravel-tags": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^9.0",
|
||||
"pestphp/pest": "^3.8",
|
||||
"pestphp/pest-plugin-laravel": "^3.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FluxCms\\Core\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"FluxCms\\Core\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"FluxCms\\Core\\FluxCmsServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
10274
packages/flux-cms/core/composer.lock
generated
Normal file
10274
packages/flux-cms/core/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
305
packages/flux-cms/core/config/flux-cms.php
Normal file
305
packages/flux-cms/core/config/flux-cms.php
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Flux CMS Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file contains the configuration options for Flux CMS.
|
||||
|
|
||||
*/
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Locale
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default locale for the CMS content.
|
||||
|
|
||||
*/
|
||||
'default_locale' => env('FLUX_CMS_DEFAULT_LOCALE', 'de'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available Locales
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The available locales for the CMS content.
|
||||
|
|
||||
*/
|
||||
'locales' => [
|
||||
'de' => 'Deutsch',
|
||||
'en' => 'English',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Paths
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The namespaces where the CMS will scan for components.
|
||||
|
|
||||
*/
|
||||
'component_paths' => [
|
||||
'App\\Livewire\\Components',
|
||||
'FluxCms\\StarterComponents\\Components',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for caching component registry and other CMS data.
|
||||
|
|
||||
*/
|
||||
'cache' => [
|
||||
'enabled' => env('FLUX_CMS_CACHE_ENABLED', true),
|
||||
'ttl' => env('FLUX_CMS_CACHE_TTL', 3600), // 1 hour
|
||||
'key_prefix' => 'flux_cms',
|
||||
'store' => env('FLUX_CMS_CACHE_STORE', null), // null = default cache store
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Database table configuration for Flux CMS.
|
||||
|
|
||||
*/
|
||||
'database' => [
|
||||
'table_prefix' => 'flux_cms_',
|
||||
'connection' => env('FLUX_CMS_DB_CONNECTION', null), // null = default connection
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication & Authorization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for user authentication and authorization.
|
||||
|
|
||||
*/
|
||||
'auth' => [
|
||||
'guard' => env('FLUX_CMS_AUTH_GUARD', 'web'),
|
||||
'default_access' => env('FLUX_CMS_DEFAULT_ACCESS', false),
|
||||
'super_admin_role' => 'admin',
|
||||
'cms_role' => 'flux-cms',
|
||||
'permissions' => [
|
||||
'view' => 'flux-cms.view',
|
||||
'edit' => 'flux-cms.edit',
|
||||
'publish' => 'flux-cms.publish',
|
||||
'delete' => 'flux-cms.delete',
|
||||
'admin' => 'flux-cms.admin',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Routes Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for CMS routes.
|
||||
|
|
||||
*/
|
||||
'routes' => [
|
||||
'enabled' => env('FLUX_CMS_ROUTES_ENABLED', true),
|
||||
'prefix' => env('FLUX_CMS_ROUTE_PREFIX', ''),
|
||||
'middleware' => ['web'],
|
||||
'admin_prefix' => env('FLUX_CMS_ADMIN_PREFIX', 'admin/cms'),
|
||||
'admin_middleware' => ['web', 'auth'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SEO Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| SEO-related configuration options.
|
||||
|
|
||||
*/
|
||||
'seo' => [
|
||||
'site_name' => env('FLUX_CMS_SITE_NAME', config('app.name')),
|
||||
'separator' => env('FLUX_CMS_SEO_SEPARATOR', ' - '),
|
||||
'meta_keywords_limit' => 10,
|
||||
'meta_description_limit' => 160,
|
||||
'auto_sitemap' => true,
|
||||
'auto_robots' => true,
|
||||
'canonical_urls' => true,
|
||||
'og_image_default' => null,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Media Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for media handling.
|
||||
|
|
||||
*/
|
||||
'media' => [
|
||||
'disk' => env('FLUX_CMS_MEDIA_DISK', 'public'),
|
||||
'max_file_size' => env('FLUX_CMS_MAX_FILE_SIZE', 10240), // 10MB in KB
|
||||
'allowed_extensions' => [
|
||||
'images' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
|
||||
'documents' => ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv'],
|
||||
'videos' => ['mp4', 'webm', 'ogg', 'avi', 'mov'],
|
||||
'audio' => ['mp3', 'wav', 'ogg', 'flac'],
|
||||
],
|
||||
'conversions' => [
|
||||
'thumb' => [
|
||||
'width' => 300,
|
||||
'height' => 300,
|
||||
'fit' => 'crop',
|
||||
],
|
||||
'medium' => [
|
||||
'width' => 800,
|
||||
'height' => 600,
|
||||
'fit' => 'contain',
|
||||
],
|
||||
'large' => [
|
||||
'width' => 1200,
|
||||
'height' => 900,
|
||||
'fit' => 'contain',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Editor Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for WYSIWYG editors.
|
||||
|
|
||||
*/
|
||||
'editor' => [
|
||||
'default' => env('FLUX_CMS_EDITOR', 'tiptap'), // tiptap, tinymce, quill
|
||||
'upload_images' => true,
|
||||
'max_image_size' => 2048, // 2MB in KB
|
||||
'toolbar_presets' => [
|
||||
'basic' => ['bold', 'italic'],
|
||||
'standard' => ['bold', 'italic', 'link', 'bulletList', 'orderedList'],
|
||||
'full' => [
|
||||
'bold', 'italic', 'underline', 'strike',
|
||||
'heading1', 'heading2', 'heading3',
|
||||
'bulletList', 'orderedList',
|
||||
'link', 'image', 'table',
|
||||
'code', 'codeBlock',
|
||||
'quote', 'rule'
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Versioning Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for content versioning.
|
||||
|
|
||||
*/
|
||||
'versioning' => [
|
||||
'enabled' => env('FLUX_CMS_VERSIONING_ENABLED', true),
|
||||
'auto_version' => env('FLUX_CMS_AUTO_VERSION', true),
|
||||
'max_versions' => env('FLUX_CMS_MAX_VERSIONS', 50),
|
||||
'cleanup_old_versions' => env('FLUX_CMS_CLEANUP_VERSIONS', true),
|
||||
'cleanup_after_days' => env('FLUX_CMS_CLEANUP_AFTER_DAYS', 365),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Performance Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Performance-related configuration options.
|
||||
|
|
||||
*/
|
||||
'performance' => [
|
||||
'eager_load_relations' => ['components', 'media'],
|
||||
'pagination_size' => 20,
|
||||
'component_registry_cache' => true,
|
||||
'query_cache_ttl' => 300, // 5 minutes
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Development Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration options for development mode.
|
||||
|
|
||||
*/
|
||||
'development' => [
|
||||
'debug_mode' => env('FLUX_CMS_DEBUG', false),
|
||||
'show_component_info' => env('FLUX_CMS_SHOW_COMPONENT_INFO', false),
|
||||
'log_queries' => env('FLUX_CMS_LOG_QUERIES', false),
|
||||
'hot_reload_components' => env('FLUX_CMS_HOT_RELOAD', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Frontend Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for frontend rendering.
|
||||
|
|
||||
*/
|
||||
'frontend' => [
|
||||
'layout' => env('FLUX_CMS_LAYOUT', 'layouts.app'),
|
||||
'theme' => env('FLUX_CMS_THEME', 'default'),
|
||||
'css_framework' => env('FLUX_CMS_CSS_FRAMEWORK', 'tailwind'), // tailwind, bootstrap
|
||||
'component_wrapper' => env('FLUX_CMS_COMPONENT_WRAPPER', true),
|
||||
'preview_mode' => env('FLUX_CMS_PREVIEW_MODE', true),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for CMS API endpoints.
|
||||
|
|
||||
*/
|
||||
'api' => [
|
||||
'enabled' => env('FLUX_CMS_API_ENABLED', false),
|
||||
'prefix' => env('FLUX_CMS_API_PREFIX', 'api/cms'),
|
||||
'middleware' => ['api', 'auth:sanctum'],
|
||||
'rate_limiting' => env('FLUX_CMS_API_RATE_LIMIT', '60,1'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Backup Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for content backups.
|
||||
|
|
||||
*/
|
||||
'backup' => [
|
||||
'enabled' => env('FLUX_CMS_BACKUP_ENABLED', true),
|
||||
'disk' => env('FLUX_CMS_BACKUP_DISK', 'local'),
|
||||
'auto_backup' => env('FLUX_CMS_AUTO_BACKUP', true),
|
||||
'backup_schedule' => env('FLUX_CMS_BACKUP_SCHEDULE', 'daily'),
|
||||
'keep_backups' => env('FLUX_CMS_KEEP_BACKUPS', 30),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Domain Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Multi-domain support configuration.
|
||||
|
|
||||
*/
|
||||
'domains' => [
|
||||
'enabled' => env('FLUX_CMS_MULTI_DOMAIN', true),
|
||||
'config_source' => 'domains', // 'domains' config key or 'database'
|
||||
'default_domain' => env('FLUX_CMS_DEFAULT_DOMAIN', 'default'),
|
||||
'auto_detect' => env('FLUX_CMS_AUTO_DETECT_DOMAIN', true),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_pages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('domain_key')->index();
|
||||
$table->json('title'); // Übersetzbarer Seitentitel
|
||||
$table->json('meta_description')->nullable(); // SEO Meta Description
|
||||
$table->json('meta_keywords')->nullable(); // SEO Keywords
|
||||
$table->json('og_image')->nullable(); // Open Graph Image (Media Library Reference)
|
||||
$table->string('canonical_url')->nullable(); // SEO Canonical URL
|
||||
$table->json('settings')->nullable(); // Zusätzliche Einstellungen
|
||||
$table->boolean('is_published')->default(true);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Composite indexes for performance
|
||||
$table->index(['domain_key', 'is_published']);
|
||||
$table->index(['domain_key', 'is_published', 'published_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_pages');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_page_components', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('page_id')->constrained('flux_cms_pages')->onDelete('cascade');
|
||||
$table->string('component_class'); // Full namespace of the component class
|
||||
$table->integer('order')->default(0);
|
||||
$table->json('content'); // Alle übersetzten Inhalte der Komponente
|
||||
$table->json('settings')->nullable(); // Komponenten-spezifische Einstellungen
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['page_id', 'order']);
|
||||
$table->index(['page_id', 'is_active']);
|
||||
$table->index(['component_class']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_page_components');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_page_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('page_id')->constrained('flux_cms_pages')->onDelete('cascade');
|
||||
$table->json('page_data'); // Snapshot der kompletten Seite
|
||||
$table->json('components_data'); // Snapshot aller Komponenten
|
||||
$table->string('version_name')->nullable(); // Benutzerdefinierter Name
|
||||
$table->text('change_description')->nullable(); // Was wurde geändert
|
||||
$table->string('created_by_type')->nullable(); // Polymorphic relation type
|
||||
$table->unsignedBigInteger('created_by_id')->nullable(); // Polymorphic relation id
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index('page_id');
|
||||
$table->index(['page_id', 'created_at']);
|
||||
$table->index(['created_by_type', 'created_by_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_page_versions');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_navigations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('domain_key')->index();
|
||||
$table->string('name'); // z.B. 'main', 'footer', 'sidebar'
|
||||
$table->json('display_name'); // Übersetzbarer Anzeigename
|
||||
$table->json('settings')->nullable(); // Navigation-spezifische Einstellungen
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
// Unique constraint
|
||||
$table->unique(['domain_key', 'name']);
|
||||
$table->index(['domain_key', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_navigations');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_navigation_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('navigation_id')->constrained('flux_cms_navigations')->onDelete('cascade');
|
||||
$table->foreignId('page_id')->nullable()->constrained('flux_cms_pages')->onDelete('cascade');
|
||||
$table->string('external_url')->nullable();
|
||||
$table->json('label'); // Übersetzbarer Menütext
|
||||
$table->integer('order')->default(0);
|
||||
$table->foreignId('parent_id')->nullable()->constrained('flux_cms_navigation_items')->onDelete('cascade');
|
||||
$table->json('settings')->nullable(); // Item-spezifische Einstellungen
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->boolean('opens_in_new_tab')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index(['navigation_id', 'order']);
|
||||
$table->index(['navigation_id', 'parent_id']);
|
||||
$table->index(['navigation_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_navigation_items');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_blog_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('domain_key')->index();
|
||||
$table->json('title'); // Übersetzbarer Titel
|
||||
$table->json('excerpt')->nullable(); // Übersetzbarer Kurztext
|
||||
$table->json('content'); // Übersetzbarer Haupttext
|
||||
$table->json('meta_description')->nullable(); // SEO
|
||||
$table->json('meta_keywords')->nullable(); // SEO
|
||||
$table->json('og_image')->nullable(); // Open Graph Image
|
||||
$table->json('settings')->nullable(); // Post-spezifische Einstellungen
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->string('author_type')->nullable(); // Polymorphic relation type
|
||||
$table->unsignedBigInteger('author_id')->nullable(); // Polymorphic relation id
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index(['domain_key', 'is_published']);
|
||||
$table->index(['domain_key', 'is_published', 'published_at']);
|
||||
$table->index(['domain_key', 'is_featured']);
|
||||
$table->index(['author_type', 'author_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_blog_posts');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('name');
|
||||
$table->json('slug');
|
||||
$table->string('type')->nullable();
|
||||
$table->integer('order_column')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('taggables', function (Blueprint $table) {
|
||||
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
|
||||
$table->morphs('taggable');
|
||||
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('taggables');
|
||||
Schema::dropIfExists('tags');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flux_cms_slugs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('model');
|
||||
$table->string('locale')->index();
|
||||
$table->string('slug');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['locale', 'slug']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flux_cms_slugs');
|
||||
}
|
||||
};
|
||||
175
packages/flux-cms/core/database/seeders/CmsContentSeeder.php
Normal file
175
packages/flux-cms/core/database/seeders/CmsContentSeeder.php
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use FluxCms\Core\Models\Page;
|
||||
use FluxCms\Core\Models\BlogPost;
|
||||
|
||||
class CmsContentSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->createSamplePages();
|
||||
$this->createSampleBlogPosts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sample pages
|
||||
*/
|
||||
protected function createSamplePages(): void
|
||||
{
|
||||
// Homepage
|
||||
$homepage = Page::create([
|
||||
'domain_key' => 'default',
|
||||
'title' => [
|
||||
'de' => 'Willkommen auf unserer Website',
|
||||
'en' => 'Welcome to our Website'
|
||||
],
|
||||
'slug' => [
|
||||
'de' => '/',
|
||||
'en' => '/'
|
||||
],
|
||||
'meta_description' => [
|
||||
'de' => 'Willkommen auf unserer modernen Website, erstellt mit Flux CMS.',
|
||||
'en' => 'Welcome to our modern website, built with Flux CMS.'
|
||||
],
|
||||
'meta_keywords' => [
|
||||
'de' => 'Website, CMS, Flux CMS, Laravel',
|
||||
'en' => 'Website, CMS, Flux CMS, Laravel'
|
||||
],
|
||||
'is_published' => true,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
// About page
|
||||
$aboutPage = Page::create([
|
||||
'domain_key' => 'default',
|
||||
'title' => [
|
||||
'de' => 'Über uns',
|
||||
'en' => 'About us'
|
||||
],
|
||||
'slug' => [
|
||||
'de' => '/ueber-uns',
|
||||
'en' => '/about'
|
||||
],
|
||||
'meta_description' => [
|
||||
'de' => 'Erfahren Sie mehr über unser Unternehmen und unsere Mission.',
|
||||
'en' => 'Learn more about our company and our mission.'
|
||||
],
|
||||
'is_published' => true,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
// Contact page
|
||||
$contactPage = Page::create([
|
||||
'domain_key' => 'default',
|
||||
'title' => [
|
||||
'de' => 'Kontakt',
|
||||
'en' => 'Contact'
|
||||
],
|
||||
'slug' => [
|
||||
'de' => '/kontakt',
|
||||
'en' => '/contact'
|
||||
],
|
||||
'meta_description' => [
|
||||
'de' => 'Kontaktieren Sie uns für weitere Informationen.',
|
||||
'en' => 'Contact us for more information.'
|
||||
],
|
||||
'is_published' => true,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$this->command->info('Sample pages created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sample blog posts
|
||||
*/
|
||||
protected function createSampleBlogPosts(): void
|
||||
{
|
||||
$posts = [
|
||||
[
|
||||
'title' => [
|
||||
'de' => 'Willkommen bei Flux CMS',
|
||||
'en' => 'Welcome to Flux CMS'
|
||||
],
|
||||
'slug' => [
|
||||
'de' => 'willkommen-bei-flux-cms',
|
||||
'en' => 'welcome-to-flux-cms'
|
||||
],
|
||||
'excerpt' => [
|
||||
'de' => 'Flux CMS ist ein modernes, komponentenbasiertes Content Management System für Laravel.',
|
||||
'en' => 'Flux CMS is a modern, component-based Content Management System for Laravel.'
|
||||
],
|
||||
'content' => [
|
||||
'de' => '<p>Flux CMS revolutioniert die Art, wie Sie Inhalte verwalten. Mit seiner einzigartigen "Code-as-Schema" Philosophie definieren Sie Inhaltsstrukturen direkt in PHP-Komponenten.</p><p>Dies bietet beispiellose Flexibilität und eine hervorragende Entwicklererfahrung.</p>',
|
||||
'en' => '<p>Flux CMS revolutionizes the way you manage content. With its unique "Code-as-Schema" philosophy, you define content structures directly in PHP components.</p><p>This offers unprecedented flexibility and an excellent developer experience.</p>'
|
||||
],
|
||||
'category' => 'News',
|
||||
'tags' => ['CMS', 'Laravel', 'Flux'],
|
||||
'is_published' => true,
|
||||
'is_featured' => true,
|
||||
'published_at' => now()->subDays(1),
|
||||
],
|
||||
[
|
||||
'title' => [
|
||||
'de' => 'Multi-Domain Support',
|
||||
'en' => 'Multi-Domain Support'
|
||||
],
|
||||
'slug' => [
|
||||
'de' => 'multi-domain-support',
|
||||
'en' => 'multi-domain-support'
|
||||
],
|
||||
'excerpt' => [
|
||||
'de' => 'Verwalten Sie mehrere Websites von einer Installation aus.',
|
||||
'en' => 'Manage multiple websites from one installation.'
|
||||
],
|
||||
'content' => [
|
||||
'de' => '<p>Mit Flux CMS können Sie mehrere Domains von einer einzigen Installation aus verwalten. Jede Domain kann ihre eigenen Inhalte, Designs und Einstellungen haben.</p>',
|
||||
'en' => '<p>With Flux CMS, you can manage multiple domains from a single installation. Each domain can have its own content, designs, and settings.</p>'
|
||||
],
|
||||
'category' => 'Features',
|
||||
'tags' => ['Multi-Domain', 'Features'],
|
||||
'is_published' => true,
|
||||
'is_featured' => false,
|
||||
'published_at' => now()->subDays(2),
|
||||
],
|
||||
[
|
||||
'title' => [
|
||||
'de' => 'Komponenten-First Architektur',
|
||||
'en' => 'Component-First Architecture'
|
||||
],
|
||||
'slug' => [
|
||||
'de' => 'komponenten-first-architektur',
|
||||
'en' => 'component-first-architecture'
|
||||
],
|
||||
'excerpt' => [
|
||||
'de' => 'Bauen Sie Seiten aus wiederverwendbaren Livewire-Komponenten.',
|
||||
'en' => 'Build pages from reusable Livewire components.'
|
||||
],
|
||||
'content' => [
|
||||
'de' => '<p>Die Komponenten-First Architektur von Flux CMS ermöglicht es Ihnen, komplexe Seiten aus kleinen, wiederverwendbaren Komponenten zu erstellen.</p><p>Jede Komponente kann ihre eigenen Felder und Validierungsregeln definieren.</p>',
|
||||
'en' => '<p>The component-first architecture of Flux CMS allows you to create complex pages from small, reusable components.</p><p>Each component can define its own fields and validation rules.</p>'
|
||||
],
|
||||
'category' => 'Architecture',
|
||||
'tags' => ['Components', 'Livewire', 'Architecture'],
|
||||
'is_published' => true,
|
||||
'is_featured' => false,
|
||||
'published_at' => now()->subDays(3),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($posts as $postData) {
|
||||
BlogPost::create(array_merge($postData, [
|
||||
'domain_key' => 'default',
|
||||
'author_id' => 1, // Assuming user ID 1 exists
|
||||
]));
|
||||
}
|
||||
|
||||
$this->command->info('Sample blog posts created successfully!');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class CmsPermissionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->createPermissions();
|
||||
$this->createRoles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CMS permissions
|
||||
*/
|
||||
protected function createPermissions(): void
|
||||
{
|
||||
$permissions = [
|
||||
'flux-cms.view' => 'View CMS content',
|
||||
'flux-cms.edit' => 'Edit CMS content',
|
||||
'flux-cms.publish' => 'Publish CMS content',
|
||||
'flux-cms.delete' => 'Delete CMS content',
|
||||
'flux-cms.admin' => 'Administer CMS',
|
||||
];
|
||||
|
||||
foreach ($permissions as $name => $description) {
|
||||
Permission::firstOrCreate(
|
||||
['name' => $name],
|
||||
['description' => $description]
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('CMS permissions created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CMS roles
|
||||
*/
|
||||
protected function createRoles(): void
|
||||
{
|
||||
// Create CMS Editor role
|
||||
$editorRole = Role::firstOrCreate(['name' => 'flux-cms-editor']);
|
||||
$editorRole->syncPermissions([
|
||||
'flux-cms.view',
|
||||
'flux-cms.edit',
|
||||
]);
|
||||
|
||||
// Create CMS Publisher role
|
||||
$publisherRole = Role::firstOrCreate(['name' => 'flux-cms-publisher']);
|
||||
$publisherRole->syncPermissions([
|
||||
'flux-cms.view',
|
||||
'flux-cms.edit',
|
||||
'flux-cms.publish',
|
||||
]);
|
||||
|
||||
// Create CMS Admin role
|
||||
$adminRole = Role::firstOrCreate(['name' => 'flux-cms-admin']);
|
||||
$adminRole->syncPermissions([
|
||||
'flux-cms.view',
|
||||
'flux-cms.edit',
|
||||
'flux-cms.publish',
|
||||
'flux-cms.delete',
|
||||
'flux-cms.admin',
|
||||
]);
|
||||
|
||||
// Create general CMS role (for backward compatibility)
|
||||
$cmsRole = Role::firstOrCreate(['name' => 'flux-cms']);
|
||||
$cmsRole->syncPermissions([
|
||||
'flux-cms.view',
|
||||
'flux-cms.edit',
|
||||
'flux-cms.publish',
|
||||
'flux-cms.delete',
|
||||
'flux-cms.admin',
|
||||
]);
|
||||
|
||||
$this->command->info('CMS roles created successfully!');
|
||||
$this->command->info('Available roles: flux-cms-editor, flux-cms-publisher, flux-cms-admin, flux-cms');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
@extends('flux-cms::admin.layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-4">Edit Blog Post</h1>
|
||||
@livewire('flux-cms::blog-editor', ['post' => $post])
|
||||
</div>
|
||||
@endsection
|
||||
185
packages/flux-cms/core/resources/views/admin/dashboard.blade.php
Normal file
185
packages/flux-cms/core/resources/views/admin/dashboard.blade.php
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
@extends('flux-cms::admin.layouts.app')
|
||||
|
||||
@section('title', 'CMS Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">CMS Dashboard</h1>
|
||||
<p class="mt-2 text-gray-600">Übersicht über Ihre Website-Inhalte</p>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Alle Seiten</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['pages'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Veröffentlichte Seiten</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['published_pages'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Entwürfe</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['draft_pages'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Blog Posts</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['blog_posts'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Veröffentlichte Posts</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ $stats['published_posts'] }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white shadow rounded-lg mb-8">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Schnellaktionen</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a href="{{ route('admin.cms.pages.create') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Neue Seite erstellen
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.blog') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||
</svg>
|
||||
Blog verwalten
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.components') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
Komponenten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Pages -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Zuletzt bearbeitete Seiten</h3>
|
||||
@if($recentPages->count() > 0)
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Titel</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Domain</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktualisiert</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($recentPages as $page)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ $page->getTranslation('title', app()->getLocale()) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $page->domain_key }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($page->is_published)
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
||||
Veröffentlicht
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||
Entwurf
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $page->updated_at->format('d.m.Y H:i') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<a href="{{ route('admin.cms.pages.edit', $page) }}" class="text-blue-600 hover:text-blue-900">Bearbeiten</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500">Noch keine Seiten vorhanden.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>@yield('title', 'Flux CMS') - {{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Styles -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<!-- Tailwind CSS (optional, remove if Tailwind already built via Vite) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans antialiased bg-gray-100">
|
||||
<div class="min-h-screen">
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<a href="{{ route('admin.cms.index') }}" class="text-xl font-bold text-gray-900">
|
||||
Flux CMS
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||
<a href="{{ route('admin.cms.index') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm {{ request()->routeIs('admin.cms.index') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.pages') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm {{ request()->routeIs('admin.cms.pages*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Seiten
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.blog') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm {{ request()->routeIs('admin.cms.blog*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Blog
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.media') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm {{ request()->routeIs('admin.cms.media*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Medien
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.components') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm {{ request()->routeIs('admin.cms.components*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Komponenten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
<!-- User dropdown -->
|
||||
<div class="ml-3 relative" x-data="{ open: false }">
|
||||
<div>
|
||||
<button @click="open = !open" class="bg-white flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<div class="h-8 w-8 rounded-full bg-gray-300 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
{{ substr(auth()->user()->name ?? 'U', 0, 1) }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-show="open" @click.away="open = false" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<a href="{{ route('admin.cms.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a>
|
||||
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Einstellungen</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
Abmelden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="-mr-2 flex items-center sm:hidden">
|
||||
<button @click="mobileMenuOpen = !mobileMenuOpen" class="bg-white inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div x-show="mobileMenuOpen" class="sm:hidden" x-data="{ mobileMenuOpen: false }">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<a href="{{ route('admin.cms.index') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('admin.cms.index') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.pages') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('admin.cms.pages*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Seiten
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.blog') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('admin.cms.blog*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Blog
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.media') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('admin.cms.media*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Medien
|
||||
</a>
|
||||
<a href="{{ route('admin.cms.components') }}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('admin.cms.components*') ? 'border-blue-500 text-gray-900' : '' }}">
|
||||
Komponenten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
@if(session('success'))
|
||||
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 5000)" class="fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 5000)" class="fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Scripts -->
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
34
packages/flux-cms/core/resources/views/fields/text.blade.php
Normal file
34
packages/flux-cms/core/resources/views/fields/text.blade.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<div class="flux-cms-field {{ $field->getCssClasses($hasError) }}">
|
||||
<label for="{{ $fieldId }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ $field->getLabel() }}
|
||||
@if($field->isRequired())
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
@if($field->isTranslatable())
|
||||
<span class="text-xs text-gray-500">({{ $locale }})</span>
|
||||
@endif
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="{{ $field->getInputType() }}"
|
||||
id="{{ $fieldId }}"
|
||||
name="{{ $field->getKey() }}"
|
||||
wire:model="{{ $wireModel }}"
|
||||
value="{{ $value }}"
|
||||
@if($field->isRequired()) required @endif
|
||||
@if($field->getMaxLength() > 0) maxlength="{{ $field->getMaxLength() }}" @endif
|
||||
@if($field->getMinLength() > 0) minlength="{{ $field->getMinLength() }}" @endif
|
||||
@if($field->getPattern()) pattern="{{ $field->getPattern() }}" @endif
|
||||
@if($field->getPlaceholder()) placeholder="{{ $field->getPlaceholder() }}" @endif
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm @if($hasError) border-red-300 @endif"
|
||||
{!! $field->getAttributes() ? ' ' . implode(' ', array_map(fn($k, $v) => "{$k}=\"{$v}\"", array_keys($field->getAttributes()), $field->getAttributes())) : '' !!}
|
||||
>
|
||||
|
||||
@if($field->getHelpText())
|
||||
<p class="mt-1 text-sm text-gray-500">{{ $field->getHelpText() }}</p>
|
||||
@endif
|
||||
|
||||
@if($hasError)
|
||||
<p class="mt-1 text-sm text-red-600">{{ $error }}</p>
|
||||
@endif
|
||||
</div>
|
||||
48
packages/flux-cms/core/resources/views/pages/show.blade.php
Normal file
48
packages/flux-cms/core/resources/views/pages/show.blade.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $page->getSeoTitle() }}</title>
|
||||
<meta name="description" content="{{ $page->getSeoDescription() }}">
|
||||
<meta name="keywords" content="{{ $page->getTranslation('meta_keywords', app()->getLocale()) }}">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="{{ $page->getTranslation('title', app()->getLocale()) }}">
|
||||
<meta property="og:description" content="{{ $page->getSeoDescription() }}">
|
||||
<meta property="og:url" content="{{ request()->url() }}">
|
||||
<meta property="og:type" content="website">
|
||||
@if($page->getTranslation('og_image', app()->getLocale()))
|
||||
<meta property="og:image" content="{{ $page->getTranslation('og_image', app()->getLocale()) }}">
|
||||
@endif
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href="{{ $page->getCanonicalUrl() }}">
|
||||
|
||||
<!-- Styles -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<!-- Tailwind CSS (optional, remove if Tailwind already built via Vite) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
@stack('head')
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<main class="cms-page">
|
||||
@foreach($components as $component)
|
||||
@if($component->canRender())
|
||||
<div class="cms-component" data-component="{{ class_basename($component->component_class) }}">
|
||||
@livewire($component->component_class, [
|
||||
'content' => $component->getTranslations('content')
|
||||
], key('component-' . $component->id))
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
14
packages/flux-cms/core/resources/views/robots.blade.php
Normal file
14
packages/flux-cms/core/resources/views/robots.blade.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
@if(config('flux-cms.seo.auto_sitemap', true))
|
||||
Sitemap: {{ $baseUrl }}/sitemap.xml
|
||||
@endif
|
||||
|
||||
# Disallow admin areas
|
||||
Disallow: /admin/
|
||||
Disallow: /preview/
|
||||
|
||||
# Disallow CMS specific paths
|
||||
Disallow: /flux-cms/
|
||||
Disallow: /vendor/
|
||||
24
packages/flux-cms/core/resources/views/sitemap.blade.php
Normal file
24
packages/flux-cms/core/resources/views/sitemap.blade.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@foreach($pages as $page)
|
||||
@foreach(config('flux-cms.locales', ['de']) as $locale => $localeName)
|
||||
<url>
|
||||
<loc>{{ $page->getUrl($locale) }}</loc>
|
||||
<lastmod>{{ $page->updated_at->format('Y-m-d') }}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>{{ $page->getTranslation('slug', $locale) === '/' ? '1.0' : '0.8' }}</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@endforeach
|
||||
|
||||
@foreach($blogPosts as $post)
|
||||
@foreach(config('flux-cms.locales', ['de']) as $locale => $localeName)
|
||||
<url>
|
||||
<loc>{{ $post->getUrl($locale) }}</loc>
|
||||
<lastmod>{{ $post->updated_at->format('Y-m-d') }}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>{{ $post->is_featured ? '0.9' : '0.7' }}</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</urlset>
|
||||
61
packages/flux-cms/core/routes/admin.php
Normal file
61
packages/flux-cms/core/routes/admin.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use FluxCms\Core\Http\Controllers\Admin\DashboardController;
|
||||
use FluxCms\Core\Http\Controllers\Admin\PageController as AdminPageController;
|
||||
use FluxCms\Core\Http\Controllers\Admin\BlogController;
|
||||
use FluxCms\Core\Http\Controllers\Admin\MediaController;
|
||||
use FluxCms\Core\Http\Controllers\Admin\NavigationController;
|
||||
use FluxCms\Core\Http\Controllers\Admin\ComponentController;
|
||||
use FluxCms\Core\Http\Controllers\PageController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Flux CMS Admin Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These routes are for the CMS admin interface. They are protected by
|
||||
| authentication and authorization middleware.
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware(['web', 'auth', 'flux-cms:cms-access'])
|
||||
->prefix(config('flux-cms.routes.admin_prefix', 'admin/cms'))
|
||||
->name('admin.cms.')
|
||||
->group(function () {
|
||||
|
||||
// Dashboard
|
||||
Route::get('/', DashboardController::class)->name('index');
|
||||
|
||||
// Pages Management
|
||||
Route::resource('pages', AdminPageController::class)->except(['show']);
|
||||
|
||||
// Blog Management
|
||||
Route::get('blog', [BlogController::class, 'index'])->name('blog.index');
|
||||
Route::get('blog/{blogPost}/edit', [BlogController::class, 'edit'])->name('blog.edit');
|
||||
|
||||
// Media Management
|
||||
Route::get('media', MediaController::class)->name('media.index');
|
||||
|
||||
// Navigation Management
|
||||
Route::get('navigation', NavigationController::class)->name('navigation.index');
|
||||
|
||||
// Component Library
|
||||
Route::get('components', ComponentController::class)->name('components.index');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Flux CMS Preview Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These routes allow authenticated users to preview unpublished content.
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware(['web', 'auth', 'flux-cms:cms-access'])
|
||||
->prefix('preview')
|
||||
->name('cms.preview.')
|
||||
->group(function () {
|
||||
Route::get('/pages/{page}', [PageController::class, 'preview'])->name('page');
|
||||
});
|
||||
30
packages/flux-cms/core/routes/web.php
Normal file
30
packages/flux-cms/core/routes/web.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use FluxCms\Core\Http\Controllers\PageController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Flux CMS Frontend Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These routes handle the frontend display of CMS content. They should be
|
||||
| placed at the END of your routes/web.php file to avoid conflicts.
|
||||
|
|
||||
*/
|
||||
|
||||
// SEO Routes
|
||||
Route::get('/sitemap.xml', [PageController::class, 'sitemap'])->name('sitemap');
|
||||
Route::get('/robots.txt', [PageController::class, 'robots'])->name('robots');
|
||||
|
||||
// Blog Routes (if using blog functionality)
|
||||
Route::prefix('blog')->name('blog.')->group(function () {
|
||||
Route::get('/', [PageController::class, 'blogIndex'])->name('index');
|
||||
Route::get('/{slug}', [PageController::class, 'blogPost'])->name('show');
|
||||
});
|
||||
|
||||
// CMS Pages - MUST BE LAST!
|
||||
// This catch-all route should be placed at the very end of your routes file
|
||||
Route::get('/{slug?}', [PageController::class, 'show'])
|
||||
->where('slug', '.*')
|
||||
->name('cms.page');
|
||||
20
packages/flux-cms/core/src/Commands/ClearCacheCommand.php
Normal file
20
packages/flux-cms/core/src/Commands/ClearCacheCommand.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use FluxCms\Core\Services\ComponentRegistry;
|
||||
|
||||
class ClearCacheCommand extends Command
|
||||
{
|
||||
protected $signature = 'flux-cms:clear-cache';
|
||||
protected $description = 'Clear the Flux CMS component registry cache';
|
||||
|
||||
public function handle(ComponentRegistry $registry): int
|
||||
{
|
||||
$this->info('Clearing Flux CMS component cache...');
|
||||
$registry->clearCache();
|
||||
$this->info('Flux CMS component cache cleared successfully!');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
185
packages/flux-cms/core/src/Commands/InstallCommand.php
Normal file
185
packages/flux-cms/core/src/Commands/InstallCommand.php
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
protected $signature = 'flux-cms:install
|
||||
{--force : Overwrite existing files}
|
||||
{--no-migrate : Skip running migrations}
|
||||
{--no-publish : Skip publishing assets}';
|
||||
|
||||
protected $description = 'Install Flux CMS';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('🚀 Installing Flux CMS...');
|
||||
|
||||
// Check requirements
|
||||
if (!$this->checkRequirements()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Publish configuration
|
||||
if (!$this->option('no-publish')) {
|
||||
$this->publishAssets();
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
if (!$this->option('no-migrate')) {
|
||||
$this->runMigrations();
|
||||
}
|
||||
|
||||
// Create storage link if needed
|
||||
$this->createStorageLink();
|
||||
|
||||
// Create sample content
|
||||
$this->createSampleContent();
|
||||
|
||||
// Set permissions
|
||||
$this->setPermissions();
|
||||
|
||||
$this->info('✅ Flux CMS installed successfully!');
|
||||
$this->showNextSteps();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function checkRequirements(): bool
|
||||
{
|
||||
$this->info('🔍 Checking requirements...');
|
||||
|
||||
$requirements = [
|
||||
'PHP 8.2+' => version_compare(PHP_VERSION, '8.2.0', '>='),
|
||||
'Laravel 11+' => $this->checkLaravelVersion(),
|
||||
'Livewire 3+' => $this->checkLivewireVersion(),
|
||||
'Spatie Translatable' => class_exists(\Spatie\Translatable\HasTranslations::class),
|
||||
'Spatie Media Library' => class_exists(\Spatie\MediaLibrary\HasMedia::class),
|
||||
];
|
||||
|
||||
$allPassed = true;
|
||||
foreach ($requirements as $requirement => $passed) {
|
||||
if ($passed) {
|
||||
$this->line("✅ {$requirement}");
|
||||
} else {
|
||||
$this->error("❌ {$requirement}");
|
||||
$allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allPassed) {
|
||||
$this->error('❌ Some requirements are not met. Please install missing dependencies.');
|
||||
$this->line('Run: composer require spatie/laravel-translatable spatie/laravel-medialibrary');
|
||||
}
|
||||
|
||||
return $allPassed;
|
||||
}
|
||||
|
||||
protected function checkLaravelVersion(): bool
|
||||
{
|
||||
return version_compare(app()->version(), '11.0.0', '>=');
|
||||
}
|
||||
|
||||
protected function checkLivewireVersion(): bool
|
||||
{
|
||||
if (!class_exists(\Livewire\Livewire::class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$version = \Livewire\Livewire::VERSION ?? '2.0.0';
|
||||
return version_compare($version, '3.0.0', '>=');
|
||||
}
|
||||
|
||||
protected function publishAssets(): void
|
||||
{
|
||||
$this->info('📦 Publishing configuration and assets...');
|
||||
|
||||
// Publish config
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-config',
|
||||
'--force' => $this->option('force'),
|
||||
]);
|
||||
|
||||
// Publish migrations
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-migrations',
|
||||
'--force' => $this->option('force'),
|
||||
]);
|
||||
|
||||
// Publish views
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-views',
|
||||
'--force' => $this->option('force'),
|
||||
]);
|
||||
|
||||
$this->line('✅ Assets published');
|
||||
}
|
||||
|
||||
protected function runMigrations(): void
|
||||
{
|
||||
$this->info('🗃️ Running migrations...');
|
||||
|
||||
$this->call('migrate');
|
||||
|
||||
$this->line('✅ Migrations completed');
|
||||
}
|
||||
|
||||
protected function createStorageLink(): void
|
||||
{
|
||||
if (!File::exists(public_path('storage'))) {
|
||||
$this->info('🔗 Creating storage link...');
|
||||
$this->call('storage:link');
|
||||
$this->line('✅ Storage link created');
|
||||
}
|
||||
}
|
||||
|
||||
protected function createSampleContent(): void
|
||||
{
|
||||
if ($this->confirm('Would you like to create sample content?', true)) {
|
||||
$this->info('📝 Creating sample content...');
|
||||
|
||||
// Run CMS content seeder
|
||||
$this->call('db:seed', ['--class' => 'FluxCms\\Core\\Database\\Seeders\\CmsContentSeeder']);
|
||||
|
||||
$this->line('✅ Sample content created');
|
||||
}
|
||||
}
|
||||
|
||||
protected function setPermissions(): void
|
||||
{
|
||||
if ($this->confirm('Would you like to create CMS permissions?', true)) {
|
||||
$this->info('🔐 Setting up permissions...');
|
||||
|
||||
// Check if Spatie Permission is available
|
||||
if (class_exists(\Spatie\Permission\Models\Permission::class)) {
|
||||
$this->createCmsPermissions();
|
||||
$this->line('✅ Permissions created');
|
||||
} else {
|
||||
$this->warn('⚠️ Spatie Permission package not found. Skipping permission setup.');
|
||||
$this->line('Install with: composer require spatie/laravel-permission');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createCmsPermissions(): void
|
||||
{
|
||||
// Run CMS permission seeder
|
||||
$this->call('db:seed', ['--class' => 'FluxCms\\Core\\Database\\Seeders\\CmsPermissionSeeder']);
|
||||
}
|
||||
|
||||
protected function showNextSteps(): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info('🎉 Next steps:');
|
||||
$this->line('1. Assign the "flux-cms" role to users who should access the CMS');
|
||||
$this->line('2. Create your first page: php artisan flux-cms:make:page');
|
||||
$this->line('3. Create custom components: php artisan flux-cms:make:component');
|
||||
$this->line('4. Visit /admin/cms to start editing');
|
||||
$this->newLine();
|
||||
$this->info('📚 Documentation: https://flux-cms.com/docs');
|
||||
$this->info('💬 Support: https://github.com/flux-cms/flux-cms/discussions');
|
||||
}
|
||||
}
|
||||
61
packages/flux-cms/core/src/Commands/MakeComponentCommand.php
Normal file
61
packages/flux-cms/core/src/Commands/MakeComponentCommand.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
class MakeComponentCommand extends GeneratorCommand
|
||||
{
|
||||
protected $name = 'flux-cms:make-component';
|
||||
protected $description = 'Create a new Flux CMS component';
|
||||
protected $type = 'Flux CMS Component';
|
||||
|
||||
protected function getStub()
|
||||
{
|
||||
return __DIR__ . '/stubs/component.stub';
|
||||
}
|
||||
|
||||
protected function getDefaultNamespace($rootNamespace)
|
||||
{
|
||||
return $rootNamespace . '\Livewire\Web\Components';
|
||||
}
|
||||
|
||||
protected function buildClass($name)
|
||||
{
|
||||
$stub = parent::buildClass($name);
|
||||
return str_replace('{{ componentName }}', $this->argument('name'), $stub);
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
{
|
||||
return [
|
||||
['name', InputArgument::REQUIRED, 'The name of the component'],
|
||||
];
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
parent::handle();
|
||||
$this->createView();
|
||||
}
|
||||
|
||||
protected function createView()
|
||||
{
|
||||
$name = $this->argument('name');
|
||||
$viewPath = resource_path('views/livewire/web/components/' . strtolower($name) . '.blade.php');
|
||||
|
||||
if (! is_dir(dirname($viewPath))) {
|
||||
mkdir(dirname($viewPath), 0777, true);
|
||||
}
|
||||
|
||||
if (file_exists($viewPath)) {
|
||||
$this->error('View already exists!');
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->files->get(__DIR__ . '/stubs/view.stub');
|
||||
$this->files->put($viewPath, $stub);
|
||||
$this->info('View created successfully.');
|
||||
}
|
||||
}
|
||||
77
packages/flux-cms/core/src/Commands/PublishCommand.php
Normal file
77
packages/flux-cms/core/src/Commands/PublishCommand.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class PublishCommand extends Command
|
||||
{
|
||||
protected $signature = 'flux-cms:publish
|
||||
{--force : Overwrite existing files}
|
||||
{--tag= : Specific tag to publish}';
|
||||
|
||||
protected $description = 'Publish Flux CMS assets and configuration';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('📦 Publishing Flux CMS assets...');
|
||||
|
||||
$tag = $this->option('tag');
|
||||
$force = $this->option('force');
|
||||
|
||||
if ($tag) {
|
||||
$this->publishTag($tag, $force);
|
||||
} else {
|
||||
$this->publishAll($force);
|
||||
}
|
||||
|
||||
$this->info('✅ Flux CMS assets published successfully!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function publishAll(bool $force): void
|
||||
{
|
||||
$this->info('Publishing all Flux CMS assets...');
|
||||
|
||||
// Publish config
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-config',
|
||||
'--force' => $force,
|
||||
]);
|
||||
|
||||
// Publish migrations
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-migrations',
|
||||
'--force' => $force,
|
||||
]);
|
||||
|
||||
// Publish views
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-views',
|
||||
'--force' => $force,
|
||||
]);
|
||||
|
||||
// Publish assets
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-assets',
|
||||
'--force' => $force,
|
||||
]);
|
||||
|
||||
// Publish translations
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => 'flux-cms-translations',
|
||||
'--force' => $force,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function publishTag(string $tag, bool $force): void
|
||||
{
|
||||
$this->info("Publishing tag: {$tag}");
|
||||
|
||||
$this->call('vendor:publish', [
|
||||
'--tag' => $tag,
|
||||
'--force' => $force,
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
packages/flux-cms/core/src/Commands/stubs/component.stub
Normal file
33
packages/flux-cms/core/src/Commands/stubs/component.stub
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Livewire\Component;
|
||||
use FluxCms\Core\FieldTypes\TextField;
|
||||
|
||||
class {{ class }} extends Component
|
||||
{
|
||||
public array $content = [];
|
||||
|
||||
public function mount(array $content = [])
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public static function getCmsName(): string
|
||||
{
|
||||
return '{{ componentName }}';
|
||||
}
|
||||
|
||||
public static function getCmsFields(): array
|
||||
{
|
||||
return [
|
||||
TextField::make('headline', 'Headline')->translatable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.web.components.{{ componentName }}');
|
||||
}
|
||||
}
|
||||
4
packages/flux-cms/core/src/Commands/stubs/view.stub
Normal file
4
packages/flux-cms/core/src/Commands/stubs/view.stub
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div>
|
||||
{{-- Remember that you should never use @section('content') in a component. --}}
|
||||
<h1>{{ $content['headline'] ?? 'Headline' }}</h1>
|
||||
</div>
|
||||
327
packages/flux-cms/core/src/FieldTypes/BaseField.php
Normal file
327
packages/flux-cms/core/src/FieldTypes/BaseField.php
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
abstract class BaseField
|
||||
{
|
||||
protected string $key;
|
||||
protected string $label;
|
||||
protected bool $translatable = false;
|
||||
protected bool $required = false;
|
||||
protected mixed $default = null;
|
||||
protected array $rules = [];
|
||||
protected array $attributes = [];
|
||||
protected ?string $helpText = null;
|
||||
protected ?string $placeholder = null;
|
||||
|
||||
public function __construct(string $key, string $label)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->label = $label;
|
||||
}
|
||||
|
||||
public static function make(string $key, string $label): static
|
||||
{
|
||||
return new static($key, $label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration Methods
|
||||
*/
|
||||
public function translatable(bool $translatable = true): static
|
||||
{
|
||||
$this->translatable = $translatable;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function required(bool $required = true): static
|
||||
{
|
||||
$this->required = $required;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function default(mixed $default): static
|
||||
{
|
||||
$this->default = $default;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function rules(array|string $rules): static
|
||||
{
|
||||
$this->rules = is_array($rules) ? $rules : [$rules];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function helpText(string $helpText): static
|
||||
{
|
||||
$this->helpText = $helpText;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function placeholder(string $placeholder): static
|
||||
{
|
||||
$this->placeholder = $placeholder;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attributes(array $attributes): static
|
||||
{
|
||||
$this->attributes = array_merge($this->attributes, $attributes);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attribute(string $key, mixed $value): static
|
||||
{
|
||||
$this->attributes[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function isTranslatable(): bool
|
||||
{
|
||||
return $this->translatable;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function getDefault(): mixed
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
public function getRules(): array
|
||||
{
|
||||
$rules = $this->rules;
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function getHelpText(): ?string
|
||||
{
|
||||
return $this->helpText;
|
||||
}
|
||||
|
||||
public function getPlaceholder(): ?string
|
||||
{
|
||||
return $this->placeholder;
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function getAttribute(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->attributes[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Methods
|
||||
*/
|
||||
abstract public function getType(): string;
|
||||
abstract public function getValidationRules(): array;
|
||||
abstract public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Value Handling
|
||||
*/
|
||||
public function getValue(array $content, string $locale = null): mixed
|
||||
{
|
||||
if ($this->translatable && $locale) {
|
||||
return $content[$this->key][$locale] ?? $this->default;
|
||||
}
|
||||
|
||||
return $content[$this->key] ?? $this->default;
|
||||
}
|
||||
|
||||
public function setValue(array &$content, mixed $value, string $locale = null): void
|
||||
{
|
||||
if ($this->translatable && $locale) {
|
||||
$content[$this->key][$locale] = $value;
|
||||
} else {
|
||||
$content[$this->key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation
|
||||
*/
|
||||
public function validate(mixed $value, string $locale = null): array
|
||||
{
|
||||
$rules = $this->getValidationRules();
|
||||
|
||||
if (empty($rules)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Für übersetzbare Felder Locale zu Regeln hinzufügen
|
||||
$fieldKey = $this->key;
|
||||
if ($locale && $this->translatable) {
|
||||
$fieldKey .= '.' . $locale;
|
||||
}
|
||||
|
||||
try {
|
||||
$validator = validator(
|
||||
[$fieldKey => $value],
|
||||
[$fieldKey => $rules],
|
||||
$this->getValidationMessages(),
|
||||
$this->getValidationAttributes()
|
||||
);
|
||||
|
||||
return $validator->fails() ? $validator->errors()->get($fieldKey) : [];
|
||||
} catch (\Exception $e) {
|
||||
return ['Validation error: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation Messages
|
||||
*/
|
||||
protected function getValidationMessages(): array
|
||||
{
|
||||
return [
|
||||
'required' => 'The :attribute field is required.',
|
||||
'string' => 'The :attribute field must be a string.',
|
||||
'integer' => 'The :attribute field must be an integer.',
|
||||
'numeric' => 'The :attribute field must be a number.',
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'array' => 'The :attribute field must be an array.',
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'max' => 'The :attribute field must not be greater than :max.',
|
||||
'min' => 'The :attribute field must be at least :min.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation Attributes
|
||||
*/
|
||||
protected function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
$this->key => $this->label,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering
|
||||
*/
|
||||
public function render(array $content = [], string $locale = null): string
|
||||
{
|
||||
$value = $this->getValue($content, $locale);
|
||||
|
||||
$viewData = [
|
||||
'field' => $this,
|
||||
'value' => $value,
|
||||
'locale' => $locale,
|
||||
'content' => $content,
|
||||
'wireModel' => $this->getWireModel($locale),
|
||||
'fieldId' => $this->getFieldId($locale),
|
||||
'hasError' => false, // Will be set by parent component
|
||||
];
|
||||
|
||||
$viewName = "flux-cms::fields.{$this->getType()}";
|
||||
|
||||
if (!view()->exists($viewName)) {
|
||||
$viewName = "flux-cms::fields.fallback";
|
||||
}
|
||||
|
||||
return view($viewName, $viewData)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire Model für Livewire
|
||||
*/
|
||||
public function getWireModel(string $locale = null): string
|
||||
{
|
||||
if ($this->translatable && $locale) {
|
||||
return "content.{$this->key}.{$locale}";
|
||||
}
|
||||
|
||||
return "content.{$this->key}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Field ID für Labels
|
||||
*/
|
||||
public function getFieldId(string $locale = null): string
|
||||
{
|
||||
$id = 'field_' . $this->key;
|
||||
|
||||
if ($this->translatable && $locale) {
|
||||
$id .= '_' . $locale;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS Classes
|
||||
*/
|
||||
public function getCssClasses(bool $hasError = false): string
|
||||
{
|
||||
$classes = ['flux-cms-field', 'flux-cms-field--' . $this->getType()];
|
||||
|
||||
if ($this->required) {
|
||||
$classes[] = 'flux-cms-field--required';
|
||||
}
|
||||
|
||||
if ($hasError) {
|
||||
$classes[] = 'flux-cms-field--error';
|
||||
}
|
||||
|
||||
if ($this->translatable) {
|
||||
$classes[] = 'flux-cms-field--translatable';
|
||||
}
|
||||
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize Input
|
||||
*/
|
||||
public function sanitizeValue(mixed $value): mixed
|
||||
{
|
||||
if (is_string($value)) {
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform für Frontend-Ausgabe
|
||||
*/
|
||||
public function transformForDisplay(mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform für Backend-Bearbeitung
|
||||
*/
|
||||
public function transformForEdit(mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
115
packages/flux-cms/core/src/FieldTypes/BooleanField.php
Normal file
115
packages/flux-cms/core/src/FieldTypes/BooleanField.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
class BooleanField extends BaseField
|
||||
{
|
||||
protected string $trueLabel = 'Yes';
|
||||
protected string $falseLabel = 'No';
|
||||
protected string $displayType = 'checkbox'; // checkbox, toggle, radio
|
||||
|
||||
public function labels(string $trueLabel, string $falseLabel): static
|
||||
{
|
||||
$this->trueLabel = $trueLabel;
|
||||
$this->falseLabel = $falseLabel;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function displayType(string $type): static
|
||||
{
|
||||
$this->displayType = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toggle(): static
|
||||
{
|
||||
$this->displayType = 'toggle';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function radio(): static
|
||||
{
|
||||
$this->displayType = 'radio';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function checkbox(): static
|
||||
{
|
||||
$this->displayType = 'checkbox';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
public function getTrueLabel(): string
|
||||
{
|
||||
return $this->trueLabel;
|
||||
}
|
||||
|
||||
public function getFalseLabel(): string
|
||||
{
|
||||
return $this->falseLabel;
|
||||
}
|
||||
|
||||
public function getDisplayType(): string
|
||||
{
|
||||
return $this->displayType;
|
||||
}
|
||||
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
$rules = ['boolean'];
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
return array_merge($rules, $this->rules);
|
||||
}
|
||||
|
||||
public function sanitizeValue(mixed $value): mixed
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert various truthy values to boolean
|
||||
if (is_string($value)) {
|
||||
$value = strtolower($value);
|
||||
return in_array($value, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
public function getValue(array $content, string $locale = null): mixed
|
||||
{
|
||||
$value = parent::getValue($content, $locale);
|
||||
return $this->sanitizeValue($value);
|
||||
}
|
||||
|
||||
public function transformForDisplay(mixed $value): mixed
|
||||
{
|
||||
return $this->sanitizeValue($value) ? $this->trueLabel : $this->falseLabel;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'key' => $this->key,
|
||||
'label' => $this->label,
|
||||
'translatable' => $this->translatable,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'trueLabel' => $this->trueLabel,
|
||||
'falseLabel' => $this->falseLabel,
|
||||
'displayType' => $this->displayType,
|
||||
'helpText' => $this->helpText,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
}
|
||||
233
packages/flux-cms/core/src/FieldTypes/MediaField.php
Normal file
233
packages/flux-cms/core/src/FieldTypes/MediaField.php
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
class MediaField extends BaseField
|
||||
{
|
||||
protected array $acceptedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
protected bool $multiple = false;
|
||||
protected int $maxFiles = 1;
|
||||
protected ?string $collection = null;
|
||||
protected array $conversions = [];
|
||||
protected int $maxFileSize = 10240; // 10MB in KB
|
||||
protected bool $showPreview = true;
|
||||
|
||||
public function acceptedMimeTypes(array $mimeTypes): static
|
||||
{
|
||||
$this->acceptedMimeTypes = $mimeTypes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function multiple(bool $multiple = true, int $maxFiles = 10): static
|
||||
{
|
||||
$this->multiple = $multiple;
|
||||
$this->maxFiles = $maxFiles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function collection(string $collection): static
|
||||
{
|
||||
$this->collection = $collection;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function conversions(array $conversions): static
|
||||
{
|
||||
$this->conversions = $conversions;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function maxFileSize(int $sizeInKb): static
|
||||
{
|
||||
$this->maxFileSize = $sizeInKb;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function showPreview(bool $show = true): static
|
||||
{
|
||||
$this->showPreview = $show;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function images(): static
|
||||
{
|
||||
$this->acceptedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'];
|
||||
$this->collection = 'images';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function documents(): static
|
||||
{
|
||||
$this->acceptedMimeTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain',
|
||||
'text/csv'
|
||||
];
|
||||
$this->collection = 'documents';
|
||||
$this->showPreview = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function videos(): static
|
||||
{
|
||||
$this->acceptedMimeTypes = ['video/mp4', 'video/webm', 'video/ogg'];
|
||||
$this->collection = 'videos';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function audio(): static
|
||||
{
|
||||
$this->acceptedMimeTypes = ['audio/mpeg', 'audio/wav', 'audio/ogg'];
|
||||
$this->collection = 'audio';
|
||||
$this->showPreview = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'media';
|
||||
}
|
||||
|
||||
public function getAcceptedMimeTypes(): array
|
||||
{
|
||||
return $this->acceptedMimeTypes;
|
||||
}
|
||||
|
||||
public function isMultiple(): bool
|
||||
{
|
||||
return $this->multiple;
|
||||
}
|
||||
|
||||
public function getMaxFiles(): int
|
||||
{
|
||||
return $this->maxFiles;
|
||||
}
|
||||
|
||||
public function getCollection(): ?string
|
||||
{
|
||||
return $this->collection;
|
||||
}
|
||||
|
||||
public function getConversions(): array
|
||||
{
|
||||
return $this->conversions;
|
||||
}
|
||||
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return $this->maxFileSize;
|
||||
}
|
||||
|
||||
public function shouldShowPreview(): bool
|
||||
{
|
||||
return $this->showPreview;
|
||||
}
|
||||
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
if ($this->multiple) {
|
||||
$rules[] = 'array';
|
||||
$rules[] = "max:{$this->maxFiles}";
|
||||
// Each item should be integer (media ID)
|
||||
$rules[] = 'array';
|
||||
foreach (range(0, $this->maxFiles - 1) as $index) {
|
||||
$rules["*.{$index}"] = 'integer|exists:media,id';
|
||||
}
|
||||
} else {
|
||||
$rules[] = 'integer';
|
||||
$rules[] = 'exists:media,id';
|
||||
}
|
||||
|
||||
return array_merge($rules, $this->rules);
|
||||
}
|
||||
|
||||
public function getAcceptAttribute(): string
|
||||
{
|
||||
return implode(',', $this->acceptedMimeTypes);
|
||||
}
|
||||
|
||||
public function isImageType(): bool
|
||||
{
|
||||
return !empty(array_filter($this->acceptedMimeTypes, fn($type) => str_starts_with($type, 'image/')));
|
||||
}
|
||||
|
||||
public function isVideoType(): bool
|
||||
{
|
||||
return !empty(array_filter($this->acceptedMimeTypes, fn($type) => str_starts_with($type, 'video/')));
|
||||
}
|
||||
|
||||
public function isAudioType(): bool
|
||||
{
|
||||
return !empty(array_filter($this->acceptedMimeTypes, fn($type) => str_starts_with($type, 'audio/')));
|
||||
}
|
||||
|
||||
public function isDocumentType(): bool
|
||||
{
|
||||
return !empty(array_filter($this->acceptedMimeTypes, fn($type) =>
|
||||
str_starts_with($type, 'application/') || str_starts_with($type, 'text/')
|
||||
));
|
||||
}
|
||||
|
||||
public function getValue(array $content, string $locale = null): mixed
|
||||
{
|
||||
$value = parent::getValue($content, $locale);
|
||||
|
||||
// Ensure array for multiple fields
|
||||
if ($this->multiple && !is_array($value)) {
|
||||
return $value ? [$value] : [];
|
||||
}
|
||||
|
||||
// Ensure integer for single fields
|
||||
if (!$this->multiple && is_array($value)) {
|
||||
return !empty($value) ? (int) $value[0] : null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getMediaItems(mixed $value): \Illuminate\Support\Collection
|
||||
{
|
||||
if (empty($value)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$mediaIds = is_array($value) ? $value : [$value];
|
||||
$mediaIds = array_filter($mediaIds);
|
||||
|
||||
if (empty($mediaIds)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return \Spatie\MediaLibrary\MediaCollections\Models\Media::whereIn('id', $mediaIds)->get();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'key' => $this->key,
|
||||
'label' => $this->label,
|
||||
'translatable' => $this->translatable,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'acceptedMimeTypes' => $this->acceptedMimeTypes,
|
||||
'multiple' => $this->multiple,
|
||||
'maxFiles' => $this->maxFiles,
|
||||
'collection' => $this->collection,
|
||||
'conversions' => $this->conversions,
|
||||
'maxFileSize' => $this->maxFileSize,
|
||||
'showPreview' => $this->showPreview,
|
||||
'helpText' => $this->helpText,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
}
|
||||
144
packages/flux-cms/core/src/FieldTypes/NumberField.php
Normal file
144
packages/flux-cms/core/src/FieldTypes/NumberField.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
class NumberField extends BaseField
|
||||
{
|
||||
protected ?float $min = null;
|
||||
protected ?float $max = null;
|
||||
protected float $step = 1;
|
||||
protected bool $decimal = false;
|
||||
|
||||
public function min(float $min): static
|
||||
{
|
||||
$this->min = $min;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function max(float $max): static
|
||||
{
|
||||
$this->max = $max;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function step(float $step): static
|
||||
{
|
||||
$this->step = $step;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function decimal(bool $decimal = true): static
|
||||
{
|
||||
$this->decimal = $decimal;
|
||||
if ($decimal && $this->step === 1) {
|
||||
$this->step = 0.01;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function currency(): static
|
||||
{
|
||||
$this->decimal(true);
|
||||
$this->step(0.01);
|
||||
$this->min(0);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function percentage(): static
|
||||
{
|
||||
$this->decimal(true);
|
||||
$this->min(0);
|
||||
$this->max(100);
|
||||
$this->step(0.1);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'number';
|
||||
}
|
||||
|
||||
public function getMin(): ?float
|
||||
{
|
||||
return $this->min;
|
||||
}
|
||||
|
||||
public function getMax(): ?float
|
||||
{
|
||||
return $this->max;
|
||||
}
|
||||
|
||||
public function getStep(): float
|
||||
{
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
public function isDecimal(): bool
|
||||
{
|
||||
return $this->decimal;
|
||||
}
|
||||
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
$rules = [$this->decimal ? 'numeric' : 'integer'];
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
if ($this->min !== null) {
|
||||
$rules[] = "min:{$this->min}";
|
||||
}
|
||||
|
||||
if ($this->max !== null) {
|
||||
$rules[] = "max:{$this->max}";
|
||||
}
|
||||
|
||||
return array_merge($rules, $this->rules);
|
||||
}
|
||||
|
||||
public function sanitizeValue(mixed $value): mixed
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->decimal) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
public function transformForDisplay(mixed $value): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->decimal) {
|
||||
return number_format((float) $value, 2);
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'key' => $this->key,
|
||||
'label' => $this->label,
|
||||
'translatable' => $this->translatable,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'min' => $this->min,
|
||||
'max' => $this->max,
|
||||
'step' => $this->step,
|
||||
'decimal' => $this->decimal,
|
||||
'helpText' => $this->helpText,
|
||||
'placeholder' => $this->placeholder,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
}
|
||||
125
packages/flux-cms/core/src/FieldTypes/SelectField.php
Normal file
125
packages/flux-cms/core/src/FieldTypes/SelectField.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
class SelectField extends BaseField
|
||||
{
|
||||
protected array $options = [];
|
||||
protected bool $multiple = false;
|
||||
protected bool $searchable = false;
|
||||
protected ?string $emptyOption = null;
|
||||
|
||||
public function options(array $options): static
|
||||
{
|
||||
$this->options = $options;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function multiple(bool $multiple = true): static
|
||||
{
|
||||
$this->multiple = $multiple;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function searchable(bool $searchable = true): static
|
||||
{
|
||||
$this->searchable = $searchable;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function emptyOption(string $text): static
|
||||
{
|
||||
$this->emptyOption = $text;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'select';
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function isMultiple(): bool
|
||||
{
|
||||
return $this->multiple;
|
||||
}
|
||||
|
||||
public function isSearchable(): bool
|
||||
{
|
||||
return $this->searchable;
|
||||
}
|
||||
|
||||
public function getEmptyOption(): ?string
|
||||
{
|
||||
return $this->emptyOption;
|
||||
}
|
||||
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
if ($this->multiple) {
|
||||
$rules[] = 'array';
|
||||
} else {
|
||||
$rules[] = 'string';
|
||||
}
|
||||
|
||||
if (!empty($this->options)) {
|
||||
$validValues = array_keys($this->options);
|
||||
if ($this->multiple) {
|
||||
$rules[] = 'array';
|
||||
// Each value must be in the valid options
|
||||
foreach ($validValues as $value) {
|
||||
$rules[] = "array";
|
||||
}
|
||||
} else {
|
||||
$rules[] = "in:" . implode(',', $validValues);
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($rules, $this->rules);
|
||||
}
|
||||
|
||||
public function getValue(array $content, string $locale = null): mixed
|
||||
{
|
||||
$value = parent::getValue($content, $locale);
|
||||
|
||||
// Ensure array for multiple selects
|
||||
if ($this->multiple && !is_array($value)) {
|
||||
return $value ? [$value] : [];
|
||||
}
|
||||
|
||||
// Ensure string for single selects
|
||||
if (!$this->multiple && is_array($value)) {
|
||||
return !empty($value) ? (string) $value[0] : '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'key' => $this->key,
|
||||
'label' => $this->label,
|
||||
'translatable' => $this->translatable,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'options' => $this->options,
|
||||
'multiple' => $this->multiple,
|
||||
'searchable' => $this->searchable,
|
||||
'emptyOption' => $this->emptyOption,
|
||||
'helpText' => $this->helpText,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
}
|
||||
166
packages/flux-cms/core/src/FieldTypes/TextField.php
Normal file
166
packages/flux-cms/core/src/FieldTypes/TextField.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
class TextField extends BaseField
|
||||
{
|
||||
protected int $maxLength = 255;
|
||||
protected int $minLength = 0;
|
||||
protected ?string $pattern = null;
|
||||
protected string $inputType = 'text';
|
||||
|
||||
public function maxLength(int $maxLength): static
|
||||
{
|
||||
$this->maxLength = $maxLength;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function minLength(int $minLength): static
|
||||
{
|
||||
$this->minLength = $minLength;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function pattern(string $pattern): static
|
||||
{
|
||||
$this->pattern = $pattern;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function email(): static
|
||||
{
|
||||
$this->inputType = 'email';
|
||||
$this->rules(['email']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function url(): static
|
||||
{
|
||||
$this->inputType = 'url';
|
||||
$this->rules(['url']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function password(): static
|
||||
{
|
||||
$this->inputType = 'password';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function tel(): static
|
||||
{
|
||||
$this->inputType = 'tel';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function search(): static
|
||||
{
|
||||
$this->inputType = 'search';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'text';
|
||||
}
|
||||
|
||||
public function getInputType(): string
|
||||
{
|
||||
return $this->inputType;
|
||||
}
|
||||
|
||||
public function getMaxLength(): int
|
||||
{
|
||||
return $this->maxLength;
|
||||
}
|
||||
|
||||
public function getMinLength(): int
|
||||
{
|
||||
return $this->minLength;
|
||||
}
|
||||
|
||||
public function getPattern(): ?string
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
$rules = ['string'];
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
if ($this->maxLength > 0) {
|
||||
$rules[] = "max:{$this->maxLength}";
|
||||
}
|
||||
|
||||
if ($this->minLength > 0) {
|
||||
$rules[] = "min:{$this->minLength}";
|
||||
}
|
||||
|
||||
// Input type specific rules
|
||||
if ($this->inputType === 'email') {
|
||||
$rules[] = 'email';
|
||||
} elseif ($this->inputType === 'url') {
|
||||
$rules[] = 'url';
|
||||
}
|
||||
|
||||
// Pattern validation
|
||||
if ($this->pattern) {
|
||||
$rules[] = "regex:{$this->pattern}";
|
||||
}
|
||||
|
||||
return array_merge($rules, $this->rules);
|
||||
}
|
||||
|
||||
public function sanitizeValue(mixed $value): mixed
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
|
||||
// Input type specific sanitization
|
||||
if ($this->inputType === 'email') {
|
||||
$value = strtolower($value);
|
||||
} elseif ($this->inputType === 'url') {
|
||||
// Ensure URL has protocol
|
||||
if ($value && !preg_match('/^https?:\/\//', $value)) {
|
||||
$value = 'https://' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'key' => $this->key,
|
||||
'label' => $this->label,
|
||||
'translatable' => $this->translatable,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'maxLength' => $this->maxLength,
|
||||
'minLength' => $this->minLength,
|
||||
'pattern' => $this->pattern,
|
||||
'inputType' => $this->inputType,
|
||||
'placeholder' => $this->placeholder,
|
||||
'helpText' => $this->helpText,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getValidationMessages(): array
|
||||
{
|
||||
return array_merge(parent::getValidationMessages(), [
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'regex' => 'The :attribute field format is invalid.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
179
packages/flux-cms/core/src/FieldTypes/WysiwygField.php
Normal file
179
packages/flux-cms/core/src/FieldTypes/WysiwygField.php
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\FieldTypes;
|
||||
|
||||
class WysiwygField extends BaseField
|
||||
{
|
||||
protected array $toolbar = ['bold', 'italic', 'link', 'bulletList', 'orderedList'];
|
||||
protected int $minHeight = 200;
|
||||
protected bool $allowImages = true;
|
||||
protected bool $allowTables = false;
|
||||
protected bool $allowCode = true;
|
||||
protected string $editor = 'tiptap'; // tiptap, tinymce, quill
|
||||
|
||||
public function toolbar(array $toolbar): static
|
||||
{
|
||||
$this->toolbar = $toolbar;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function minHeight(int $minHeight): static
|
||||
{
|
||||
$this->minHeight = $minHeight;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function allowImages(bool $allowImages = true): static
|
||||
{
|
||||
$this->allowImages = $allowImages;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function allowTables(bool $allowTables = true): static
|
||||
{
|
||||
$this->allowTables = $allowTables;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function allowCode(bool $allowCode = true): static
|
||||
{
|
||||
$this->allowCode = $allowCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function editor(string $editor): static
|
||||
{
|
||||
$this->editor = $editor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function basic(): static
|
||||
{
|
||||
$this->toolbar = ['bold', 'italic'];
|
||||
$this->allowImages = false;
|
||||
$this->allowTables = false;
|
||||
$this->allowCode = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function full(): static
|
||||
{
|
||||
$this->toolbar = [
|
||||
'bold', 'italic', 'underline', 'strike',
|
||||
'heading1', 'heading2', 'heading3',
|
||||
'bulletList', 'orderedList',
|
||||
'link', 'image', 'table',
|
||||
'code', 'codeBlock',
|
||||
'quote', 'rule'
|
||||
];
|
||||
$this->allowImages = true;
|
||||
$this->allowTables = true;
|
||||
$this->allowCode = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'wysiwyg';
|
||||
}
|
||||
|
||||
public function getToolbar(): array
|
||||
{
|
||||
return $this->toolbar;
|
||||
}
|
||||
|
||||
public function getMinHeight(): int
|
||||
{
|
||||
return $this->minHeight;
|
||||
}
|
||||
|
||||
public function getAllowImages(): bool
|
||||
{
|
||||
return $this->allowImages;
|
||||
}
|
||||
|
||||
public function getAllowTables(): bool
|
||||
{
|
||||
return $this->allowTables;
|
||||
}
|
||||
|
||||
public function getAllowCode(): bool
|
||||
{
|
||||
return $this->allowCode;
|
||||
}
|
||||
|
||||
public function getEditor(): string
|
||||
{
|
||||
return $this->editor;
|
||||
}
|
||||
|
||||
public function getValidationRules(): array
|
||||
{
|
||||
$rules = ['string'];
|
||||
|
||||
if ($this->required) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
return array_merge($rules, $this->rules);
|
||||
}
|
||||
|
||||
public function sanitizeValue(mixed $value): mixed
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Basic HTML sanitization
|
||||
$value = trim($value);
|
||||
|
||||
// Remove dangerous tags
|
||||
$dangerousTags = ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'];
|
||||
foreach ($dangerousTags as $tag) {
|
||||
$value = preg_replace('/<' . $tag . '[^>]*>.*?<\/' . $tag . '>/is', '', $value);
|
||||
$value = preg_replace('/<' . $tag . '[^>]*\/?>/is', '', $value);
|
||||
}
|
||||
|
||||
// Remove javascript: links
|
||||
$value = preg_replace('/javascript:/i', '', $value);
|
||||
|
||||
// Remove on* attributes
|
||||
$value = preg_replace('/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', '', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function transformForDisplay(mixed $value): mixed
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Convert relative URLs to absolute
|
||||
$value = preg_replace_callback('/src="(\/[^"]*)"/', function ($matches) {
|
||||
return 'src="' . url($matches[1]) . '"';
|
||||
}, $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'key' => $this->key,
|
||||
'label' => $this->label,
|
||||
'translatable' => $this->translatable,
|
||||
'required' => $this->required,
|
||||
'default' => $this->default,
|
||||
'toolbar' => $this->toolbar,
|
||||
'minHeight' => $this->minHeight,
|
||||
'allowImages' => $this->allowImages,
|
||||
'allowTables' => $this->allowTables,
|
||||
'allowCode' => $this->allowCode,
|
||||
'editor' => $this->editor,
|
||||
'helpText' => $this->helpText,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
}
|
||||
222
packages/flux-cms/core/src/FluxCmsServiceProvider.php
Normal file
222
packages/flux-cms/core/src/FluxCmsServiceProvider.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use FluxCms\Core\Services\ComponentRegistry;
|
||||
use FluxCms\Core\Commands\InstallCommand;
|
||||
use FluxCms\Core\Commands\PublishCommand;
|
||||
use FluxCms\Core\Commands\ClearCacheCommand;
|
||||
use FluxCms\Core\Commands\MakeComponentCommand;
|
||||
|
||||
class FluxCmsServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Merge config
|
||||
$this->mergeConfigFrom(__DIR__ . '/../config/flux-cms.php', 'flux-cms');
|
||||
|
||||
// Register services
|
||||
$this->app->singleton(ComponentRegistry::class, function ($app) {
|
||||
return new ComponentRegistry();
|
||||
});
|
||||
|
||||
// Register aliases
|
||||
$this->app->alias(ComponentRegistry::class, 'flux-cms.registry');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->bootPublishing();
|
||||
$this->bootMigrations();
|
||||
$this->bootViews();
|
||||
$this->bootCommands();
|
||||
$this->bootRoutes();
|
||||
$this->bootMiddleware();
|
||||
$this->bootGates();
|
||||
$this->bootTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot publishing
|
||||
*/
|
||||
protected function bootPublishing(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
// Publish config
|
||||
$this->publishes([
|
||||
__DIR__ . '/../config/flux-cms.php' => config_path('flux-cms.php'),
|
||||
], 'flux-cms-config');
|
||||
|
||||
// Publish migrations
|
||||
$this->publishes([
|
||||
__DIR__ . '/../database/migrations' => database_path('migrations'),
|
||||
], 'flux-cms-migrations');
|
||||
|
||||
// Publish views
|
||||
$this->publishes([
|
||||
__DIR__ . '/../resources/views' => resource_path('views/vendor/flux-cms'),
|
||||
], 'flux-cms-views');
|
||||
|
||||
// Publish all
|
||||
$this->publishes([
|
||||
__DIR__ . '/../config/flux-cms.php' => config_path('flux-cms.php'),
|
||||
__DIR__ . '/../database/migrations' => database_path('migrations'),
|
||||
__DIR__ . '/../resources/views' => resource_path('views/vendor/flux-cms'),
|
||||
], 'flux-cms');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot migrations
|
||||
*/
|
||||
protected function bootMigrations(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot views
|
||||
*/
|
||||
protected function bootViews(): void
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'flux-cms');
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot commands
|
||||
*/
|
||||
protected function bootCommands(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
InstallCommand::class,
|
||||
PublishCommand::class,
|
||||
ClearCacheCommand::class,
|
||||
MakeComponentCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot routes
|
||||
*/
|
||||
protected function bootRoutes(): void
|
||||
{
|
||||
if (config('flux-cms.routes.enabled', true)) {
|
||||
$this->loadRoutesFromDirectory(__DIR__ . '/../routes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot middleware
|
||||
*/
|
||||
protected function bootMiddleware(): void
|
||||
{
|
||||
$router = $this->app['router'];
|
||||
|
||||
// Register middleware aliases
|
||||
$router->aliasMiddleware('flux-cms:cms-access', \FluxCms\Core\Http\Middleware\CmsAccess::class);
|
||||
$router->aliasMiddleware('flux-cms:domain-detection', \FluxCms\Core\Http\Middleware\DomainDetection::class);
|
||||
$router->aliasMiddleware('flux-cms:preview-mode', \FluxCms\Core\Http\Middleware\PreviewMode::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load routes from directory
|
||||
*/
|
||||
protected function loadRoutesFromDirectory(string $directory): void
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($directory . '/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->loadRoutesFrom($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot authorization gates
|
||||
*/
|
||||
protected function bootGates(): void
|
||||
{
|
||||
Gate::define('flux-cms.view', function ($user) {
|
||||
return $this->userHasCmsPermission($user, 'view');
|
||||
});
|
||||
|
||||
Gate::define('flux-cms.edit', function ($user) {
|
||||
return $this->userHasCmsPermission($user, 'edit');
|
||||
});
|
||||
|
||||
Gate::define('flux-cms.publish', function ($user) {
|
||||
return $this->userHasCmsPermission($user, 'publish');
|
||||
});
|
||||
|
||||
Gate::define('flux-cms.delete', function ($user) {
|
||||
return $this->userHasCmsPermission($user, 'delete');
|
||||
});
|
||||
|
||||
Gate::define('flux-cms.admin', function ($user) {
|
||||
return $this->userHasCmsPermission($user, 'admin');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has CMS permission
|
||||
*/
|
||||
protected function userHasCmsPermission($user, string $permission): bool
|
||||
{
|
||||
// If no user, deny access
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for Spatie Permission package
|
||||
if (method_exists($user, 'can')) {
|
||||
return $user->can("flux-cms.{$permission}") || $user->hasRole('flux-cms') || $user->hasRole('admin');
|
||||
}
|
||||
|
||||
// Fallback: Check if user has admin role property
|
||||
if (isset($user->is_admin)) {
|
||||
return $user->is_admin;
|
||||
}
|
||||
|
||||
// Default: Allow access for authenticated users (can be overridden in config)
|
||||
return config('flux-cms.auth.default_access', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot translations
|
||||
*/
|
||||
protected function bootTranslations(): void
|
||||
{
|
||||
$this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'flux-cms');
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->publishes([
|
||||
__DIR__ . '/../resources/lang' => resource_path('lang/vendor/flux-cms'),
|
||||
], 'flux-cms-translations');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [
|
||||
ComponentRegistry::class,
|
||||
'flux-cms.registry',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use FluxCms\Core\Models\BlogPost;
|
||||
|
||||
class BlogController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('can:flux-cms.view');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = BlogPost::with(['author']);
|
||||
|
||||
if ($request->has('status')) {
|
||||
switch ($request->status) {
|
||||
case 'published':
|
||||
$query->published();
|
||||
break;
|
||||
case 'draft':
|
||||
$query->where('is_published', false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$posts = $query->orderBy('updated_at', 'desc')->paginate(20);
|
||||
|
||||
return view('flux-cms::admin.blog.index', compact('posts'));
|
||||
}
|
||||
|
||||
public function edit(BlogPost $blogPost)
|
||||
{
|
||||
$this->authorize('flux-cms.edit');
|
||||
return view('flux-cms::admin.blog.edit', ['post' => $blogPost]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use FluxCms\Core\Services\ComponentRegistry;
|
||||
|
||||
class ComponentController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('can:flux-cms.view');
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
$registry = app(ComponentRegistry::class);
|
||||
$components = $registry->getComponentsByCategory();
|
||||
$stats = $registry->getComponentStats();
|
||||
|
||||
return view('flux-cms::admin.components.index', compact('components', 'stats'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use FluxCms\Core\Models\Page;
|
||||
use FluxCms\Core\Models\BlogPost;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
$stats = [
|
||||
'pages' => Page::count(),
|
||||
'published_pages' => Page::published()->count(),
|
||||
'draft_pages' => Page::where('is_published', false)->count(),
|
||||
'blog_posts' => BlogPost::count(),
|
||||
'published_posts' => BlogPost::published()->count(),
|
||||
];
|
||||
|
||||
$recentPages = Page::with(['components'])
|
||||
->orderBy('updated_at', 'desc')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
return view('flux-cms::admin.dashboard', compact('stats', 'recentPages'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('can:flux-cms.view');
|
||||
}
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
// This would integrate with Spatie Media Library
|
||||
return view('flux-cms::admin.media.index');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class NavigationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('can:flux-cms.view');
|
||||
}
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
return view('flux-cms::admin.navigation.index');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use FluxCms\Core\Models\Page;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('can:flux-cms.view');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Page::with(['components']);
|
||||
|
||||
if ($request->has('domain') && $request->domain) {
|
||||
$query->forDomain($request->domain);
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
switch ($request->status) {
|
||||
case 'published':
|
||||
$query->published();
|
||||
break;
|
||||
case 'draft':
|
||||
$query->where('is_published', false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$pages = $query->orderBy('updated_at', 'desc')->paginate(20);
|
||||
|
||||
return view('flux-cms::admin.pages.index', compact('pages'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$this->authorize('flux-cms.edit');
|
||||
$domains = $this->getAvailableDomains();
|
||||
$locales = config('flux-cms.locales');
|
||||
return view('flux-cms::admin.pages.create', compact('domains', 'locales'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorize('flux-cms.edit');
|
||||
|
||||
$validated = $request->validate([
|
||||
'domain_key' => 'required|string',
|
||||
'title' => 'required|array',
|
||||
'title.*' => 'required|string|max:255',
|
||||
'slugs' => 'required|array',
|
||||
'slugs.*' => 'required|string',
|
||||
]);
|
||||
|
||||
$page = Page::create([
|
||||
'domain_key' => $validated['domain_key'],
|
||||
'title' => $validated['title'],
|
||||
'is_published' => $request->boolean('is_published'),
|
||||
'published_at' => $request->boolean('is_published') ? now() : null,
|
||||
]);
|
||||
|
||||
foreach ($validated['slugs'] as $locale => $slug) {
|
||||
$page->slugs()->create(['locale' => $locale, 'slug' => $slug]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.cms.pages.edit', $page)
|
||||
->with('success', 'Page created successfully!');
|
||||
}
|
||||
|
||||
public function edit(Page $page)
|
||||
{
|
||||
$this->authorize('flux-cms.edit');
|
||||
return view('flux-cms::admin.pages.edit', compact('page'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Page $page)
|
||||
{
|
||||
$this->authorize('flux-cms.edit');
|
||||
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|array',
|
||||
'title.*' => 'required|string|max:255',
|
||||
'slugs' => 'required|array',
|
||||
'slugs.*' => 'required|string',
|
||||
]);
|
||||
|
||||
$page->update([
|
||||
'title' => $validated['title'],
|
||||
'is_published' => $request->boolean('is_published'),
|
||||
'published_at' => $request->boolean('is_published') && !$page->published_at ? now() : $page->published_at,
|
||||
]);
|
||||
|
||||
foreach ($validated['slugs'] as $locale => $slug) {
|
||||
$page->slugs()->updateOrCreate(
|
||||
['locale' => $locale],
|
||||
['slug' => $slug]
|
||||
);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.cms.pages.edit', $page)
|
||||
->with('success', 'Page updated successfully!');
|
||||
}
|
||||
|
||||
public function destroy(Page $page)
|
||||
{
|
||||
$this->authorize('flux-cms.delete');
|
||||
$page->delete();
|
||||
return redirect()->route('admin.cms.pages.index')->with('success', 'Page deleted successfully!');
|
||||
}
|
||||
|
||||
private function getAvailableDomains(): array
|
||||
{
|
||||
if (!config('flux-cms.domains.enabled')) {
|
||||
return ['default' => 'Default'];
|
||||
}
|
||||
|
||||
$domains = config('domains.domains', []);
|
||||
$result = [];
|
||||
|
||||
foreach ($domains as $key => $config) {
|
||||
$result[$key] = $config['name'] ?? $key;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
153
packages/flux-cms/core/src/Http/Controllers/PageController.php
Normal file
153
packages/flux-cms/core/src/Http/Controllers/PageController.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use FluxCms\Core\Models\Page;
|
||||
use FluxCms\Core\Models\BlogPost;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a CMS page
|
||||
*/
|
||||
public function show(Request $request, string $slug = '/')
|
||||
{
|
||||
$domainKey = $this->getCurrentDomainKey($request);
|
||||
|
||||
$page = Page::forDomain($domainKey)
|
||||
->published()
|
||||
->bySlugWithFallback($slug)
|
||||
->with(['components'])
|
||||
->firstOrFail();
|
||||
|
||||
// Load active components
|
||||
$components = $page->components()->get();
|
||||
|
||||
return view('flux-cms::pages.show', compact('page', 'components'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Blog index page
|
||||
*/
|
||||
public function blogIndex(Request $request)
|
||||
{
|
||||
$domainKey = $this->getCurrentDomainKey($request);
|
||||
$locale = app()->getLocale();
|
||||
|
||||
$posts = BlogPost::forDomain($domainKey)
|
||||
->published()
|
||||
->orderBy('published_at', 'desc')
|
||||
->paginate(10);
|
||||
|
||||
return view('flux-cms::blog.index', compact('posts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Blog post page
|
||||
*/
|
||||
public function blogPost(Request $request, string $slug)
|
||||
{
|
||||
$domainKey = $this->getCurrentDomainKey($request);
|
||||
|
||||
$post = BlogPost::forDomain($domainKey)
|
||||
->published()
|
||||
->bySlugWithFallback($slug)
|
||||
->firstOrFail();
|
||||
|
||||
// Get related posts
|
||||
$relatedPosts = BlogPost::forDomain($domainKey)
|
||||
->published()
|
||||
->where('id', '!=', $post->id)
|
||||
->where('category', $post->category)
|
||||
->limit(3)
|
||||
->get();
|
||||
|
||||
return view('flux-cms::blog.show', compact('post', 'relatedPosts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate sitemap
|
||||
*/
|
||||
public function sitemap(Request $request)
|
||||
{
|
||||
$domainKey = $this->getCurrentDomainKey($request);
|
||||
$locale = app()->getLocale();
|
||||
|
||||
$pages = Page::forDomain($domainKey)
|
||||
->published()
|
||||
->get();
|
||||
|
||||
$blogPosts = BlogPost::forDomain($domainKey)
|
||||
->published()
|
||||
->get();
|
||||
|
||||
return response()->view('flux-cms::sitemap', compact('pages', 'blogPosts'))
|
||||
->header('Content-Type', 'application/xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate robots.txt
|
||||
*/
|
||||
public function robots(Request $request)
|
||||
{
|
||||
$domainKey = $this->getCurrentDomainKey($request);
|
||||
$baseUrl = $this->getBaseUrl($request);
|
||||
|
||||
return response()->view('flux-cms::robots', compact('baseUrl'))
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview page (for authenticated users)
|
||||
*/
|
||||
public function preview(Request $request, Page $page)
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('can:flux-cms.view');
|
||||
|
||||
$components = $page->allComponents()->get();
|
||||
|
||||
return view('flux-cms::pages.preview', compact('page', 'components'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current domain key
|
||||
*/
|
||||
protected function getCurrentDomainKey(Request $request): string
|
||||
{
|
||||
if (!config('flux-cms.domains.enabled')) {
|
||||
return config('flux-cms.domains.default_domain', 'default');
|
||||
}
|
||||
|
||||
$host = $request->getHost();
|
||||
$domains = config('domains.domains', []);
|
||||
|
||||
foreach ($domains as $key => $config) {
|
||||
if (isset($config['url'])) {
|
||||
$domainHost = parse_url($config['url'], PHP_URL_HOST);
|
||||
if ($domainHost === $host) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config('flux-cms.domains.default_domain', 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base URL for current domain
|
||||
*/
|
||||
protected function getBaseUrl(Request $request): string
|
||||
{
|
||||
$domainKey = $this->getCurrentDomainKey($request);
|
||||
$domains = config('domains.domains', []);
|
||||
|
||||
if (isset($domains[$domainKey]['url'])) {
|
||||
return $domains[$domainKey]['url'];
|
||||
}
|
||||
|
||||
return $request->getSchemeAndHttpHost();
|
||||
}
|
||||
}
|
||||
51
packages/flux-cms/core/src/Http/Middleware/CmsAccess.php
Normal file
51
packages/flux-cms/core/src/Http/Middleware/CmsAccess.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CmsAccess
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string $permission = 'view'): Response
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
// Check if user has CMS permission
|
||||
if (!$this->userHasCmsPermission($user, $permission)) {
|
||||
abort(403, 'Access denied. You do not have permission to access the CMS.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has CMS permission
|
||||
*/
|
||||
protected function userHasCmsPermission($user, string $permission): bool
|
||||
{
|
||||
// Check for Spatie Permission package
|
||||
if (method_exists($user, 'can')) {
|
||||
return $user->can("flux-cms.{$permission}") ||
|
||||
$user->hasRole('flux-cms') ||
|
||||
$user->hasRole('admin');
|
||||
}
|
||||
|
||||
// Fallback: Check if user has admin role property
|
||||
if (isset($user->is_admin)) {
|
||||
return $user->is_admin;
|
||||
}
|
||||
|
||||
// Default: Allow access for authenticated users (can be overridden in config)
|
||||
return config('flux-cms.auth.default_access', false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DomainDetection
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!config('flux-cms.domains.enabled')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$domainKey = $this->detectDomainKey($request);
|
||||
|
||||
// Set domain key in request for later use
|
||||
$request->attributes->set('flux_cms_domain_key', $domainKey);
|
||||
|
||||
// Set locale based on domain if configured
|
||||
$this->setLocaleFromDomain($request, $domainKey);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect domain key from request
|
||||
*/
|
||||
protected function detectDomainKey(Request $request): string
|
||||
{
|
||||
$host = $request->getHost();
|
||||
$domains = config('domains.domains', []);
|
||||
|
||||
foreach ($domains as $key => $config) {
|
||||
if (isset($config['url'])) {
|
||||
$domainHost = parse_url($config['url'], PHP_URL_HOST);
|
||||
if ($domainHost === $host) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config('flux-cms.domains.default_domain', 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set locale based on domain configuration
|
||||
*/
|
||||
protected function setLocaleFromDomain(Request $request, string $domainKey): void
|
||||
{
|
||||
$domains = config('domains.domains', []);
|
||||
|
||||
if (isset($domains[$domainKey]['locale'])) {
|
||||
$locale = $domains[$domainKey]['locale'];
|
||||
app()->setLocale($locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
packages/flux-cms/core/src/Http/Middleware/PreviewMode.php
Normal file
55
packages/flux-cms/core/src/Http/Middleware/PreviewMode.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PreviewMode
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Check if preview mode is enabled via query parameter
|
||||
if ($request->has('preview') && $request->boolean('preview')) {
|
||||
// Verify user has preview permission
|
||||
if (!$this->userCanPreview($request)) {
|
||||
abort(403, 'Preview access denied');
|
||||
}
|
||||
|
||||
// Set preview mode in request
|
||||
$request->attributes->set('flux_cms_preview_mode', true);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can preview content
|
||||
*/
|
||||
protected function userCanPreview(Request $request): bool
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for Spatie Permission package
|
||||
if (method_exists($user, 'can')) {
|
||||
return $user->can('flux-cms.view') ||
|
||||
$user->hasRole('flux-cms') ||
|
||||
$user->hasRole('admin');
|
||||
}
|
||||
|
||||
// Fallback: Check if user has admin role property
|
||||
if (isset($user->is_admin)) {
|
||||
return $user->is_admin;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
257
packages/flux-cms/core/src/Models/BlogPost.php
Normal file
257
packages/flux-cms/core/src/Models/BlogPost.php
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Spatie\Tags\HasTags;
|
||||
|
||||
class BlogPost extends Model implements HasMedia
|
||||
{
|
||||
use HasTranslations, InteractsWithMedia, HasTags;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('flux-cms.database.table_prefix', 'flux_cms_') . 'blog_posts';
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'domain_key',
|
||||
'title',
|
||||
'excerpt',
|
||||
'content',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'is_published',
|
||||
'is_featured',
|
||||
'published_at',
|
||||
'author_id',
|
||||
'category',
|
||||
'settings',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'excerpt',
|
||||
'content',
|
||||
'meta_title',
|
||||
'meta_description'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'title' => 'array',
|
||||
'excerpt' => 'array',
|
||||
'content' => 'array',
|
||||
'meta_title' => 'array',
|
||||
'meta_description' => 'array',
|
||||
'settings' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'is_featured' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slug relationship
|
||||
*/
|
||||
public function slugs()
|
||||
{
|
||||
return $this->morphMany(Slug::class, 'model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForDomain($query, string $domainKey)
|
||||
{
|
||||
return $query->where('domain_key', $domainKey);
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('is_published', true)
|
||||
->where('published_at', '<=', now());
|
||||
}
|
||||
|
||||
public function scopeFeatured($query)
|
||||
{
|
||||
return $query->where('is_featured', true);
|
||||
}
|
||||
|
||||
public function scopeByCategory($query, string $category)
|
||||
{
|
||||
return $query->where('category', $category);
|
||||
}
|
||||
|
||||
public function scopeByTag($query, string $tag)
|
||||
{
|
||||
return $query->withAnyTags([$tag]);
|
||||
}
|
||||
|
||||
public function scopeRecent($query, int $limit = 10)
|
||||
{
|
||||
return $query->orderBy('published_at', 'desc')->limit($limit);
|
||||
}
|
||||
|
||||
public function scopeBySlug($query, string $slug, string $locale = null)
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
return $query->whereHas('slugs', function ($q) use ($slug, $locale) {
|
||||
$q->where('slug', $slug)->where('locale', $locale);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeBySlugWithFallback($query, string $slug, string $locale = null)
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$fallbackLocale = config('app.fallback_locale');
|
||||
|
||||
return $query->where(function ($q) use ($slug, $locale, $fallbackLocale) {
|
||||
$q->whereHas('slugs', function ($sq) use ($slug, $locale) {
|
||||
$sq->where('slug', $slug)->where('locale', $locale);
|
||||
})->orWhereHas('slugs', function ($sq) use ($slug, $fallbackLocale) {
|
||||
$sq->where('slug', $slug)->where('locale', $fallbackLocale);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for this blog post
|
||||
*/
|
||||
public function getUrl(string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$slug = $this->slugs()->where('locale', $locale)->first();
|
||||
|
||||
return route('blog.post', ['slug' => $slug->slug ?? '']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reading time estimate in minutes
|
||||
*/
|
||||
public function getReadingTimeAttribute(): int
|
||||
{
|
||||
$content = strip_tags($this->getTranslation('content', app()->getLocale()));
|
||||
$wordCount = str_word_count($content);
|
||||
return max(1, ceil($wordCount / 200)); // Assuming 200 words per minute
|
||||
}
|
||||
|
||||
/**
|
||||
* Get excerpt with fallback to content
|
||||
*/
|
||||
public function getExcerptAttribute($value): string
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$excerpt = $this->getTranslation('excerpt', $locale);
|
||||
|
||||
if (empty($excerpt)) {
|
||||
$content = strip_tags($this->getTranslation('content', $locale));
|
||||
$excerpt = str_limit($content, 160);
|
||||
}
|
||||
|
||||
return $excerpt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if post is published
|
||||
*/
|
||||
public function isPublished(): bool
|
||||
{
|
||||
return $this->is_published &&
|
||||
$this->published_at &&
|
||||
$this->published_at <= now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SEO title with fallback to title
|
||||
*/
|
||||
public function getSeoTitle(string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
|
||||
return $this->getTranslation('meta_title', $locale)
|
||||
?? $this->getTranslation('title', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SEO description with fallback to excerpt
|
||||
*/
|
||||
public function getSeoDescription(string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
|
||||
return $this->getTranslation('meta_description', $locale)
|
||||
?? $this->getTranslation('excerpt', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Media collections
|
||||
*/
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('featured_image')
|
||||
->singleFile()
|
||||
->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);
|
||||
|
||||
$this->addMediaCollection('gallery')
|
||||
->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);
|
||||
}
|
||||
|
||||
public function registerMediaConversions(Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('thumb')
|
||||
->width(300)
|
||||
->height(200)
|
||||
->sharpen(10);
|
||||
|
||||
$this->addMediaConversion('hero')
|
||||
->width(1200)
|
||||
->height(600)
|
||||
->optimize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured image
|
||||
*/
|
||||
public function getFeaturedImage(): ?Media
|
||||
{
|
||||
return $this->getFirstMedia('featured_image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured image URL
|
||||
*/
|
||||
public function getFeaturedImageUrl(string $conversion = ''): ?string
|
||||
{
|
||||
$media = $this->getFeaturedImage();
|
||||
|
||||
if (!$media) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $conversion ? $media->getUrl($conversion) : $media->getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings management
|
||||
*/
|
||||
public function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
public function setSetting(string $key, mixed $value): void
|
||||
{
|
||||
$settings = $this->settings ?? [];
|
||||
data_set($settings, $key, $value);
|
||||
$this->update(['settings' => $settings]);
|
||||
}
|
||||
}
|
||||
92
packages/flux-cms/core/src/Models/Navigation.php
Normal file
92
packages/flux-cms/core/src/Models/Navigation.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
|
||||
class Navigation extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('flux-cms.database.table_prefix', 'flux_cms_') . 'navigations';
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'domain_key',
|
||||
'name',
|
||||
'display_name',
|
||||
'settings',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'display_name'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'display_name' => 'array',
|
||||
'settings' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(NavigationItem::class, 'navigation_id')
|
||||
->where('is_active', true)
|
||||
->whereNull('parent_id')
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
public function allItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(NavigationItem::class, 'navigation_id')
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForDomain($query, string $domainKey)
|
||||
{
|
||||
return $query->where('domain_key', $domainKey);
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeByName($query, string $name, string $domainKey)
|
||||
{
|
||||
return $query->where('name', $name)->where('domain_key', $domainKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hierarchical navigation structure
|
||||
*/
|
||||
public function getHierarchicalItems(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->items()->with(['children' => function ($query) {
|
||||
$query->where('is_active', true)->orderBy('order');
|
||||
}, 'page'])->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings management
|
||||
*/
|
||||
public function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
public function setSetting(string $key, mixed $value): void
|
||||
{
|
||||
$settings = $this->settings ?? [];
|
||||
data_set($settings, $key, $value);
|
||||
$this->update(['settings' => $settings]);
|
||||
}
|
||||
}
|
||||
140
packages/flux-cms/core/src/Models/NavigationItem.php
Normal file
140
packages/flux-cms/core/src/Models/NavigationItem.php
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
|
||||
class NavigationItem extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('flux-cms.database.table_prefix', 'flux_cms_') . 'navigation_items';
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'navigation_id',
|
||||
'parent_id',
|
||||
'page_id',
|
||||
'title',
|
||||
'url',
|
||||
'target',
|
||||
'order',
|
||||
'is_active',
|
||||
'settings',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'title' => 'array',
|
||||
'settings' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'order' => 'integer',
|
||||
];
|
||||
|
||||
public function navigation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Navigation::class, 'navigation_id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(NavigationItem::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(NavigationItem::class, 'parent_id')
|
||||
->where('is_active', true)
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
public function allChildren(): HasMany
|
||||
{
|
||||
return $this->hasMany(NavigationItem::class, 'parent_id')
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
public function page(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'page_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeTopLevel($query)
|
||||
{
|
||||
return $query->whereNull('parent_id');
|
||||
}
|
||||
|
||||
public function scopeForNavigation($query, int $navigationId)
|
||||
{
|
||||
return $query->where('navigation_id', $navigationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective URL for this navigation item
|
||||
*/
|
||||
public function getEffectiveUrl(): string
|
||||
{
|
||||
if ($this->page_id && $this->page) {
|
||||
return $this->page->getUrl();
|
||||
}
|
||||
|
||||
return $this->url ?? '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this item has children
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return $this->children()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant IDs (children, grandchildren, etc.)
|
||||
*/
|
||||
public function getDescendantIds(): array
|
||||
{
|
||||
$ids = [];
|
||||
$this->collectDescendantIds($ids);
|
||||
return $ids;
|
||||
}
|
||||
|
||||
protected function collectDescendantIds(array &$ids): void
|
||||
{
|
||||
foreach ($this->allChildren as $child) {
|
||||
$ids[] = $child->id;
|
||||
$child->collectDescendantIds($ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings management
|
||||
*/
|
||||
public function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
public function setSetting(string $key, mixed $value): void
|
||||
{
|
||||
$settings = $this->settings ?? [];
|
||||
data_set($settings, $key, $value);
|
||||
$this->update(['settings' => $settings]);
|
||||
}
|
||||
}
|
||||
241
packages/flux-cms/core/src/Models/Page.php
Normal file
241
packages/flux-cms/core/src/Models/Page.php
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
|
||||
class Page extends Model implements HasMedia
|
||||
{
|
||||
use HasTranslations, InteractsWithMedia;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('flux-cms.database.table_prefix', 'flux_cms_') . 'pages';
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'domain_key',
|
||||
'title',
|
||||
'meta_description',
|
||||
'meta_keywords',
|
||||
'og_image',
|
||||
'canonical_url',
|
||||
'is_published',
|
||||
'published_at',
|
||||
'settings',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'meta_description',
|
||||
'meta_keywords',
|
||||
'og_image'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'title' => 'array',
|
||||
'meta_description' => 'array',
|
||||
'meta_keywords' => 'array',
|
||||
'og_image' => 'array',
|
||||
'settings' => 'array',
|
||||
'is_published' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function components(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageComponent::class, 'page_id')
|
||||
->where('is_active', true)
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
public function allComponents(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageComponent::class, 'page_id')
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageVersion::class, 'page_id')
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function navigationItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(NavigationItem::class, 'page_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Media Collections für SEO Images
|
||||
*/
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('og_images')
|
||||
->singleFile()
|
||||
->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);
|
||||
|
||||
$this->addMediaCollection('page_assets')
|
||||
->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml']);
|
||||
}
|
||||
|
||||
public function registerMediaConversions(Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('og_thumb')
|
||||
->width(1200)
|
||||
->height(630)
|
||||
->sharpen(10);
|
||||
|
||||
$this->addMediaConversion('preview')
|
||||
->width(400)
|
||||
->height(300)
|
||||
->sharpen(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForDomain($query, string $domainKey)
|
||||
{
|
||||
return $query->where('domain_key', $domainKey);
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('is_published', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('published_at')
|
||||
->orWhere('published_at', '<=', now());
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeBySlug($query, string $slug, string $locale = null)
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
return $query->whereHas('slugs', function ($q) use ($slug, $locale) {
|
||||
$q->where('slug', $slug)->where('locale', $locale);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeBySlugWithFallback($query, string $slug, string $locale = null)
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$fallbackLocale = config('app.fallback_locale');
|
||||
|
||||
return $query->where(function ($q) use ($slug, $locale, $fallbackLocale) {
|
||||
$q->whereHas('slugs', function ($sq) use ($slug, $locale) {
|
||||
$sq->where('slug', $slug)->where('locale', $locale);
|
||||
})->orWhereHas('slugs', function ($sq) use ($slug, $fallbackLocale) {
|
||||
$sq->where('slug', $slug)->where('locale', $fallbackLocale);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Slug relationship
|
||||
*/
|
||||
public function slugs()
|
||||
{
|
||||
return $this->morphMany(Slug::class, 'model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Version Management
|
||||
*/
|
||||
public function createVersion(string $changeDescription = null, ?Model $user = null): PageVersion
|
||||
{
|
||||
$version = $this->versions()->make([
|
||||
'page_data' => $this->toArray(),
|
||||
'components_data' => $this->allComponents()->with('media')->get()->toArray(),
|
||||
'change_description' => $changeDescription,
|
||||
'version_name' => $this->generateVersionName(),
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
$version->createdBy()->associate($user);
|
||||
}
|
||||
|
||||
$version->save();
|
||||
return $version;
|
||||
}
|
||||
|
||||
protected function generateVersionName(): string
|
||||
{
|
||||
$count = $this->versions()->count();
|
||||
return "Version " . ($count + 1) . " - " . now()->format('Y-m-d H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishing Methods
|
||||
*/
|
||||
public function publish(): void
|
||||
{
|
||||
$this->update([
|
||||
'is_published' => true,
|
||||
'published_at' => $this->published_at ?? now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function unpublish(): void
|
||||
{
|
||||
$this->update(['is_published' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO Methods
|
||||
*/
|
||||
public function getSeoTitle(string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$title = $this->getTranslation('title', $locale);
|
||||
$siteName = config('flux-cms.seo.site_name', config('app.name'));
|
||||
|
||||
return $title ? "{$title} - {$siteName}" : $siteName;
|
||||
}
|
||||
|
||||
public function getSeoDescription(string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
return $this->getTranslation('meta_description', $locale) ?? '';
|
||||
}
|
||||
|
||||
public function getCanonicalUrl(): string
|
||||
{
|
||||
return $this->canonical_url ?: request()->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL Generation
|
||||
*/
|
||||
public function getUrl(string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$slug = $this->slugs()->where('locale', $locale)->first();
|
||||
|
||||
if (!$slug || $slug->slug === '/') {
|
||||
return url('/');
|
||||
}
|
||||
|
||||
return url(ltrim($slug->slug, '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings Management
|
||||
*/
|
||||
public function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
public function setSetting(string $key, mixed $value): void
|
||||
{
|
||||
$settings = $this->settings ?? [];
|
||||
data_set($settings, $key, $value);
|
||||
$this->update(['settings' => $settings]);
|
||||
}
|
||||
}
|
||||
217
packages/flux-cms/core/src/Models/PageComponent.php
Normal file
217
packages/flux-cms/core/src/Models/PageComponent.php
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use FluxCms\Core\Services\ComponentRegistry;
|
||||
|
||||
class PageComponent extends Model implements HasMedia
|
||||
{
|
||||
use HasTranslations, InteractsWithMedia;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('flux-cms.database.table_prefix', 'flux_cms_') . 'page_components';
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'page_id',
|
||||
'component_class',
|
||||
'order',
|
||||
'content',
|
||||
'is_active',
|
||||
'settings',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'content'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'content' => 'array',
|
||||
'settings' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function page(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'page_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Media Collections für Component Assets
|
||||
*/
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('component_images')
|
||||
->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml']);
|
||||
|
||||
$this->addMediaCollection('component_files')
|
||||
->acceptsMimeTypes([
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain'
|
||||
]);
|
||||
|
||||
$this->addMediaCollection('component_videos')
|
||||
->acceptsMimeTypes(['video/mp4', 'video/webm', 'video/ogg']);
|
||||
}
|
||||
|
||||
public function registerMediaConversions(Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('thumb')
|
||||
->width(300)
|
||||
->height(300)
|
||||
->sharpen(10);
|
||||
|
||||
$this->addMediaConversion('medium')
|
||||
->width(800)
|
||||
->height(600)
|
||||
->sharpen(10);
|
||||
|
||||
$this->addMediaConversion('large')
|
||||
->width(1200)
|
||||
->height(900)
|
||||
->sharpen(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Configuration
|
||||
*/
|
||||
public function getComponentConfig(): array
|
||||
{
|
||||
if (!class_exists($this->component_class)) {
|
||||
return [
|
||||
'name' => 'Unknown Component',
|
||||
'fields' => [],
|
||||
'category' => 'Unknown',
|
||||
'error' => 'Component class not found: ' . $this->component_class
|
||||
];
|
||||
}
|
||||
|
||||
$registry = app(ComponentRegistry::class);
|
||||
return $registry->getComponentConfig($this->component_class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Validation
|
||||
*/
|
||||
public function validateContent(): array
|
||||
{
|
||||
if (!class_exists($this->component_class)) {
|
||||
return ['component_class' => 'Component class not found'];
|
||||
}
|
||||
|
||||
$registry = app(ComponentRegistry::class);
|
||||
return $registry->validateComponentContent($this->component_class, $this->content ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('order');
|
||||
}
|
||||
|
||||
public function scopeByComponentClass($query, string $componentClass)
|
||||
{
|
||||
return $query->where('component_class', $componentClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Management
|
||||
*/
|
||||
public function getTranslatedContent(string $locale = null): array
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$content = $this->getTranslations('content');
|
||||
|
||||
return $content[$locale] ?? $content[config('app.fallback_locale')] ?? [];
|
||||
}
|
||||
|
||||
public function setTranslatedContent(array $content, string $locale = null): void
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
$translations = $this->getTranslations('content');
|
||||
$translations[$locale] = $content;
|
||||
$this->content = $translations;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function getContentValue(string $key, string $locale = null): mixed
|
||||
{
|
||||
$content = $this->getTranslatedContent($locale);
|
||||
return data_get($content, $key);
|
||||
}
|
||||
|
||||
public function setContentValue(string $key, mixed $value, string $locale = null): void
|
||||
{
|
||||
$content = $this->getTranslatedContent($locale);
|
||||
data_set($content, $key, $value);
|
||||
$this->setTranslatedContent($content, $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Rendering
|
||||
*/
|
||||
public function canRender(): bool
|
||||
{
|
||||
return class_exists($this->component_class) && $this->is_active;
|
||||
}
|
||||
|
||||
public function getComponentName(): string
|
||||
{
|
||||
$config = $this->getComponentConfig();
|
||||
return $config['name'] ?? class_basename($this->component_class);
|
||||
}
|
||||
|
||||
public function getComponentCategory(): string
|
||||
{
|
||||
$config = $this->getComponentConfig();
|
||||
return $config['category'] ?? 'General';
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplication
|
||||
*/
|
||||
public function duplicate(int $newOrder = null): self
|
||||
{
|
||||
$duplicate = $this->replicate();
|
||||
$duplicate->order = $newOrder ?? ($this->page->allComponents()->max('order') + 1);
|
||||
$duplicate->save();
|
||||
|
||||
// Copy media if any
|
||||
foreach ($this->getMedia() as $media) {
|
||||
$media->copy($duplicate);
|
||||
}
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings Management
|
||||
*/
|
||||
public function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
public function setSetting(string $key, mixed $value): void
|
||||
{
|
||||
$settings = $this->settings ?? [];
|
||||
data_set($settings, $key, $value);
|
||||
$this->update(['settings' => $settings]);
|
||||
}
|
||||
}
|
||||
101
packages/flux-cms/core/src/Models/PageVersion.php
Normal file
101
packages/flux-cms/core/src/Models/PageVersion.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PageVersion extends Model
|
||||
{
|
||||
public function getTable()
|
||||
{
|
||||
return config('flux-cms.database.table_prefix', 'flux_cms_') . 'page_versions';
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'page_id',
|
||||
'page_data',
|
||||
'components_data',
|
||||
'version_name',
|
||||
'change_description',
|
||||
'created_by_type',
|
||||
'created_by_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'page_data' => 'array',
|
||||
'components_data' => 'array',
|
||||
];
|
||||
|
||||
public function page(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'page_id');
|
||||
}
|
||||
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->morphTo('created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore this version
|
||||
*/
|
||||
public function restore(): void
|
||||
{
|
||||
$page = $this->page;
|
||||
|
||||
// Create backup of current version
|
||||
$page->createVersion('Backup before restoration');
|
||||
|
||||
// Restore page data
|
||||
$pageData = $this->page_data;
|
||||
unset($pageData['id'], $pageData['created_at'], $pageData['updated_at']);
|
||||
$page->update($pageData);
|
||||
|
||||
// Delete current components and restore from version
|
||||
$page->allComponents()->delete();
|
||||
|
||||
foreach ($this->components_data as $componentData) {
|
||||
unset($componentData['id'], $componentData['page_id'], $componentData['created_at'], $componentData['updated_at']);
|
||||
$page->allComponents()->create($componentData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get differences compared to current version
|
||||
*/
|
||||
public function getDifferences(): array
|
||||
{
|
||||
$currentPage = $this->page->fresh();
|
||||
$currentComponents = $currentPage->allComponents()->get();
|
||||
|
||||
return [
|
||||
'page_changes' => $this->arrayDiff($this->page_data, $currentPage->toArray()),
|
||||
'components_changes' => $this->arrayDiff($this->components_data, $currentComponents->toArray()),
|
||||
];
|
||||
}
|
||||
|
||||
private function arrayDiff(array $old, array $new): array
|
||||
{
|
||||
$changes = [];
|
||||
|
||||
foreach ($old as $key => $value) {
|
||||
if (!isset($new[$key])) {
|
||||
$changes['removed'][$key] = $value;
|
||||
} elseif ($new[$key] !== $value) {
|
||||
$changes['changed'][$key] = [
|
||||
'old' => $value,
|
||||
'new' => $new[$key]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($new as $key => $value) {
|
||||
if (!isset($old[$key])) {
|
||||
$changes['added'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
}
|
||||
22
packages/flux-cms/core/src/Models/Slug.php
Normal file
22
packages/flux-cms/core/src/Models/Slug.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Slug extends Model
|
||||
{
|
||||
protected $table = 'flux_cms_slugs';
|
||||
|
||||
protected $fillable = [
|
||||
'model_type',
|
||||
'model_id',
|
||||
'locale',
|
||||
'slug',
|
||||
];
|
||||
|
||||
public function model()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
470
packages/flux-cms/core/src/Services/ComponentRegistry.php
Normal file
470
packages/flux-cms/core/src/Services/ComponentRegistry.php
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use FluxCms\Core\FieldTypes\BaseField;
|
||||
|
||||
class ComponentRegistry
|
||||
{
|
||||
protected array $componentPaths = [];
|
||||
protected string $cacheKey = 'flux_cms.component.registry';
|
||||
protected int $cacheTtl = 3600; // 1 Stunde
|
||||
protected bool $cacheEnabled = true;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->componentPaths = config('flux-cms.component_paths', [
|
||||
'App\\Livewire\\Components',
|
||||
'FluxCms\\StarterComponents\\Components',
|
||||
]);
|
||||
|
||||
$this->cacheEnabled = config('flux-cms.cache.components', true);
|
||||
$this->cacheTtl = config('flux-cms.cache.ttl', 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole alle verfügbaren CMS-Komponenten
|
||||
*/
|
||||
public function getAvailableComponents(): array
|
||||
{
|
||||
if (!$this->cacheEnabled) {
|
||||
return $this->scanComponents();
|
||||
}
|
||||
|
||||
return Cache::remember($this->cacheKey, $this->cacheTtl, function () {
|
||||
return $this->scanComponents();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole eine spezifische Komponente
|
||||
*/
|
||||
public function getComponent(string $className): ?array
|
||||
{
|
||||
$components = $this->getAvailableComponents();
|
||||
return $components[$className] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüfe ob eine Klasse eine gültige CMS-Komponente ist
|
||||
*/
|
||||
public function isValidComponent(string $className): bool
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
// Muss getCmsFields Methode haben
|
||||
if (!$reflection->hasMethod('getCmsFields')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// getCmsFields muss static sein
|
||||
$method = $reflection->getMethod('getCmsFields');
|
||||
if (!$method->isStatic() || !$method->isPublic()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Muss von Livewire\Component erben
|
||||
if (!$reflection->isSubclassOf(\Livewire\Component::class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (ReflectionException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole die CMS-Konfiguration einer Komponente
|
||||
*/
|
||||
public function getComponentConfig(string $className): array
|
||||
{
|
||||
if (!$this->isValidComponent($className)) {
|
||||
return [
|
||||
'class' => $className,
|
||||
'name' => 'Invalid Component',
|
||||
'fields' => [],
|
||||
'category' => 'Error',
|
||||
'error' => 'Component is not valid or does not exist'
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$config = [
|
||||
'class' => $className,
|
||||
'name' => $this->getComponentName($className),
|
||||
'fields' => $className::getCmsFields(),
|
||||
'category' => $this->getComponentCategory($className),
|
||||
'description' => $this->getComponentDescription($className),
|
||||
'preview' => $this->getComponentPreview($className),
|
||||
'icon' => $this->getComponentIcon($className),
|
||||
'tags' => $this->getComponentTags($className),
|
||||
'version' => $this->getComponentVersion($className),
|
||||
];
|
||||
|
||||
// Validiere Felder
|
||||
$config['fields'] = $this->validateFields($config['fields']);
|
||||
|
||||
return $config;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("Error getting component config for {$className}: " . $e->getMessage());
|
||||
return [
|
||||
'class' => $className,
|
||||
'name' => 'Error Component',
|
||||
'fields' => [],
|
||||
'category' => 'Error',
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scanne alle Komponenten-Verzeichnisse
|
||||
*/
|
||||
protected function scanComponents(): array
|
||||
{
|
||||
$components = [];
|
||||
|
||||
foreach ($this->componentPaths as $namespace) {
|
||||
$path = $this->namespaceToPath($namespace);
|
||||
|
||||
if (!is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$components = array_merge($components, $this->scanDirectory($path, $namespace));
|
||||
}
|
||||
|
||||
// Sortiere nach Kategorie und Name
|
||||
uasort($components, function ($a, $b) {
|
||||
$categoryCompare = strcmp($a['category'], $b['category']);
|
||||
if ($categoryCompare !== 0) {
|
||||
return $categoryCompare;
|
||||
}
|
||||
return strcmp($a['name'], $b['name']);
|
||||
});
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scanne ein Verzeichnis nach Komponenten
|
||||
*/
|
||||
protected function scanDirectory(string $path, string $namespace): array
|
||||
{
|
||||
$components = [];
|
||||
|
||||
if (!is_dir($path)) {
|
||||
return $components;
|
||||
}
|
||||
|
||||
$files = File::allFiles($path);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = $this->fileToClassName($file, $namespace, $path);
|
||||
|
||||
if ($this->isValidComponent($className)) {
|
||||
$config = $this->getComponentConfig($className);
|
||||
if (!isset($config['error'])) {
|
||||
$components[$className] = $config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiere Namespace zu Dateipfad
|
||||
*/
|
||||
protected function namespaceToPath(string $namespace): string
|
||||
{
|
||||
// Handle App namespace
|
||||
if (str_starts_with($namespace, 'App\\')) {
|
||||
$path = str_replace('App\\', 'app/', $namespace);
|
||||
$path = str_replace('\\', '/', $path);
|
||||
return base_path($path);
|
||||
}
|
||||
|
||||
// Handle package namespaces
|
||||
if (str_starts_with($namespace, 'FluxCms\\')) {
|
||||
$path = str_replace('FluxCms\\', 'packages/flux-cms/', $namespace);
|
||||
$path = str_replace('\\', '/', $path);
|
||||
$path = strtolower($path);
|
||||
return base_path($path . '/src');
|
||||
}
|
||||
|
||||
// Fallback
|
||||
$path = str_replace('\\', '/', $namespace);
|
||||
return base_path('vendor/' . strtolower($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiere Datei zu Klassenname
|
||||
*/
|
||||
protected function fileToClassName(\SplFileInfo $file, string $namespace, string $basePath): string
|
||||
{
|
||||
$relativePath = str_replace($basePath . '/', '', $file->getPathname());
|
||||
$relativePath = str_replace('.php', '', $relativePath);
|
||||
$relativePath = str_replace('/', '\\', $relativePath);
|
||||
|
||||
return $namespace . '\\' . $relativePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Metadaten-Methoden
|
||||
*/
|
||||
protected function getComponentName(string $className): string
|
||||
{
|
||||
if (method_exists($className, 'getCmsName')) {
|
||||
return $className::getCmsName();
|
||||
}
|
||||
return $this->classNameToReadable($className);
|
||||
}
|
||||
|
||||
protected function getComponentCategory(string $className): string
|
||||
{
|
||||
if (method_exists($className, 'getCmsCategory')) {
|
||||
return $className::getCmsCategory();
|
||||
}
|
||||
return 'General';
|
||||
}
|
||||
|
||||
protected function getComponentDescription(string $className): ?string
|
||||
{
|
||||
if (method_exists($className, 'getCmsDescription')) {
|
||||
return $className::getCmsDescription();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getComponentPreview(string $className): ?string
|
||||
{
|
||||
if (method_exists($className, 'getCmsPreview')) {
|
||||
return $className::getCmsPreview();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getComponentIcon(string $className): string
|
||||
{
|
||||
if (method_exists($className, 'getCmsIcon')) {
|
||||
return $className::getCmsIcon();
|
||||
}
|
||||
return 'puzzle-piece';
|
||||
}
|
||||
|
||||
protected function getComponentTags(string $className): array
|
||||
{
|
||||
if (method_exists($className, 'getCmsTags')) {
|
||||
return $className::getCmsTags();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getComponentVersion(string $className): string
|
||||
{
|
||||
if (method_exists($className, 'getCmsVersion')) {
|
||||
return $className::getCmsVersion();
|
||||
}
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiere Feld-Definitionen
|
||||
*/
|
||||
protected function validateFields(array $fields): array
|
||||
{
|
||||
$validatedFields = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if ($field instanceof BaseField) {
|
||||
$validatedFields[] = $field;
|
||||
} else {
|
||||
\Log::warning('Invalid field type in component fields', [
|
||||
'field' => $field,
|
||||
'type' => gettype($field)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $validatedFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiere Klassenname zu lesbarem Namen
|
||||
*/
|
||||
protected function classNameToReadable(string $className): string
|
||||
{
|
||||
$baseName = class_basename($className);
|
||||
return preg_replace('/([a-z])([A-Z])/', '$1 $2', $baseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiere Komponenten-Content
|
||||
*/
|
||||
public function validateComponentContent(string $className, array $content): array
|
||||
{
|
||||
if (!$this->isValidComponent($className)) {
|
||||
return ['component' => ['Invalid component class: ' . $className]];
|
||||
}
|
||||
|
||||
// Custom Validation der Komponente
|
||||
if (method_exists($className, 'validateContent')) {
|
||||
$customErrors = $className::validateContent($content);
|
||||
if (!empty($customErrors)) {
|
||||
return $customErrors;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard-Validation basierend auf Feldern
|
||||
return $this->validateContentByFields($className, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiere Content anhand der Feld-Definitionen
|
||||
*/
|
||||
protected function validateContentByFields(string $className, array $content): array
|
||||
{
|
||||
$errors = [];
|
||||
$fields = $className::getCmsFields();
|
||||
$availableLocales = config('flux-cms.locales', ['de', 'en']);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (!$field instanceof BaseField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($field->isTranslatable()) {
|
||||
// Validiere für alle verfügbaren Sprachen
|
||||
foreach ($availableLocales as $locale) {
|
||||
$value = $content[$field->getKey()][$locale] ?? null;
|
||||
$fieldErrors = $field->validate($value, $locale);
|
||||
if (!empty($fieldErrors)) {
|
||||
$errors["{$field->getKey()}.{$locale}"] = $fieldErrors;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$value = $content[$field->getKey()] ?? null;
|
||||
$fieldErrors = $field->validate($value);
|
||||
if (!empty($fieldErrors)) {
|
||||
$errors[$field->getKey()] = $fieldErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache Management
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
Cache::forget($this->cacheKey);
|
||||
}
|
||||
|
||||
public function refreshCache(): array
|
||||
{
|
||||
$this->clearCache();
|
||||
return $this->getAvailableComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Path Management
|
||||
*/
|
||||
public function addComponentPath(string $namespace): void
|
||||
{
|
||||
if (!in_array($namespace, $this->componentPaths)) {
|
||||
$this->componentPaths[] = $namespace;
|
||||
$this->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeComponentPath(string $namespace): void
|
||||
{
|
||||
$this->componentPaths = array_filter($this->componentPaths, fn($path) => $path !== $namespace);
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
public function getComponentPaths(): array
|
||||
{
|
||||
return $this->componentPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole Komponenten nach Kategorie
|
||||
*/
|
||||
public function getComponentsByCategory(): array
|
||||
{
|
||||
$components = $this->getAvailableComponents();
|
||||
$categorized = [];
|
||||
|
||||
foreach ($components as $component) {
|
||||
$category = $component['category'] ?? 'General';
|
||||
$categorized[$category][] = $component;
|
||||
}
|
||||
|
||||
return $categorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suche Komponenten
|
||||
*/
|
||||
public function searchComponents(string $query): array
|
||||
{
|
||||
$components = $this->getAvailableComponents();
|
||||
$query = strtolower($query);
|
||||
|
||||
return array_filter($components, function ($component) use ($query) {
|
||||
$searchFields = [
|
||||
strtolower($component['name']),
|
||||
strtolower($component['category']),
|
||||
strtolower($component['description'] ?? ''),
|
||||
strtolower(implode(' ', $component['tags'] ?? []))
|
||||
];
|
||||
|
||||
foreach ($searchFields as $field) {
|
||||
if (str_contains($field, $query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole Komponenten-Statistiken
|
||||
*/
|
||||
public function getComponentStats(): array
|
||||
{
|
||||
$components = $this->getAvailableComponents();
|
||||
|
||||
$stats = [
|
||||
'total' => count($components),
|
||||
'categories' => [],
|
||||
'most_used' => [],
|
||||
];
|
||||
|
||||
foreach ($components as $component) {
|
||||
$category = $component['category'] ?? 'General';
|
||||
$stats['categories'][$category] = ($stats['categories'][$category] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
32
packages/flux-cms/core/tests/Browser/LoginTest.php
Normal file
32
packages/flux-cms/core/tests/Browser/LoginTest.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Browser;
|
||||
|
||||
use FluxCms\Core\Models\User;
|
||||
use FluxCms\Core\Tests\DuskTestCase;
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
class LoginTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic browser test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_admin_can_login_successfully()
|
||||
{
|
||||
$admin = User::factory()->create([
|
||||
'email' => 'admin@flux-cms.com',
|
||||
'password' => bcrypt('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($admin) {
|
||||
$browser->visit('/login')
|
||||
->type('email', $admin->email)
|
||||
->type('password', 'password')
|
||||
->press('Login')
|
||||
->assertPathIs('/admin/cms');
|
||||
});
|
||||
}
|
||||
}
|
||||
49
packages/flux-cms/core/tests/DuskTestCase.php
Normal file
49
packages/flux-cms/core/tests/DuskTestCase.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests;
|
||||
|
||||
use Laravel\Dusk\TestCase as BaseTestCase;
|
||||
use Orchestra\Testbench\Concerns\CreatesApplication;
|
||||
use Facebook\WebDriver\Chrome\ChromeOptions;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
use CreatesApplication;
|
||||
|
||||
/**
|
||||
* Prepare for Dusk test execution.
|
||||
*
|
||||
* @beforeClass
|
||||
* @return void
|
||||
*/
|
||||
public static function prepare()
|
||||
{
|
||||
if (! static::runningInSail()) {
|
||||
static::startChromeDriver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the RemoteWebDriver instance.
|
||||
*
|
||||
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
|
||||
*/
|
||||
protected function driver()
|
||||
{
|
||||
$options = (new ChromeOptions)->addArguments([
|
||||
'--disable-gpu',
|
||||
'--headless',
|
||||
'--window-size=1920,1080',
|
||||
]);
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
$_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
|
||||
DesiredCapabilities::chrome()->setCapability(
|
||||
ChromeOptions::CAPABILITY,
|
||||
$options
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
197
packages/flux-cms/core/tests/Feature/PageManagementTest.php
Normal file
197
packages/flux-cms/core/tests/Feature/PageManagementTest.php
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Feature;
|
||||
|
||||
use FluxCms\Core\Models\Page;
|
||||
use FluxCms\Core\Models\PageComponent;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class PageManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->loadLaravelMigrations();
|
||||
$this->artisan('migrate');
|
||||
}
|
||||
|
||||
public function test_can_create_page()
|
||||
{
|
||||
$page = Page::create([
|
||||
'domain_key' => 'test',
|
||||
'title' => ['de' => 'Test Seite', 'en' => 'Test Page'],
|
||||
'slug' => ['de' => '/test-seite', 'en' => '/test-page'],
|
||||
'is_published' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('flux_cms_pages', [
|
||||
'id' => $page->id,
|
||||
'domain_key' => 'test',
|
||||
]);
|
||||
|
||||
$this->assertEquals('Test Seite', $page->getTranslation('title', 'de'));
|
||||
$this->assertEquals('Test Page', $page->getTranslation('title', 'en'));
|
||||
}
|
||||
|
||||
public function test_can_add_components_to_page()
|
||||
{
|
||||
$page = Page::create([
|
||||
'domain_key' => 'test',
|
||||
'title' => ['de' => 'Test Seite'],
|
||||
'slug' => ['de' => '/test'],
|
||||
'is_published' => true,
|
||||
]);
|
||||
|
||||
$component = $page->allComponents()->create([
|
||||
'component_class' => 'TestComponent',
|
||||
'order' => 1,
|
||||
'content' => [
|
||||
'title' => ['de' => 'Komponenten Titel'],
|
||||
'text' => ['de' => 'Komponenten Text']
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('flux_cms_page_components', [
|
||||
'page_id' => $page->id,
|
||||
'component_class' => 'TestComponent',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$this->assertEquals(1, $page->allComponents()->count());
|
||||
$this->assertEquals('Komponenten Titel', $component->getTranslatedContent('de')['title']);
|
||||
}
|
||||
|
||||
public function test_can_publish_and_unpublish_page()
|
||||
{
|
||||
$page = Page::create([
|
||||
'domain_key' => 'test',
|
||||
'title' => ['de' => 'Test Seite'],
|
||||
'slug' => ['de' => '/test'],
|
||||
'is_published' => false,
|
||||
]);
|
||||
|
||||
$this->assertFalse($page->is_published);
|
||||
|
||||
$page->publish();
|
||||
$this->assertTrue($page->fresh()->is_published);
|
||||
$this->assertNotNull($page->fresh()->published_at);
|
||||
|
||||
$page->unpublish();
|
||||
$this->assertFalse($page->fresh()->is_published);
|
||||
}
|
||||
|
||||
public function test_can_create_page_version()
|
||||
{
|
||||
$page = Page::create([
|
||||
'domain_key' => 'test',
|
||||
'title' => ['de' => 'Test Seite'],
|
||||
'slug' => ['de' => '/test'],
|
||||
'is_published' => true,
|
||||
]);
|
||||
|
||||
$page->allComponents()->create([
|
||||
'component_class' => 'TestComponent',
|
||||
'order' => 1,
|
||||
'content' => ['title' => ['de' => 'Original']],
|
||||
]);
|
||||
|
||||
$version = $page->createVersion('Initial version');
|
||||
|
||||
$this->assertDatabaseHas('flux_cms_page_versions', [
|
||||
'page_id' => $page->id,
|
||||
'change_description' => 'Initial version',
|
||||
]);
|
||||
|
||||
$this->assertNotNull($version->page_data);
|
||||
$this->assertNotNull($version->components_data);
|
||||
$this->assertEquals(1, $page->versions()->count());
|
||||
}
|
||||
|
||||
public function test_page_scope_by_domain()
|
||||
{
|
||||
Page::create([
|
||||
'domain_key' => 'domain1',
|
||||
'title' => ['de' => 'Domain 1 Seite'],
|
||||
'slug' => ['de' => '/domain1'],
|
||||
]);
|
||||
|
||||
Page::create([
|
||||
'domain_key' => 'domain2',
|
||||
'title' => ['de' => 'Domain 2 Seite'],
|
||||
'slug' => ['de' => '/domain2'],
|
||||
]);
|
||||
|
||||
$domain1Pages = Page::forDomain('domain1')->get();
|
||||
$domain2Pages = Page::forDomain('domain2')->get();
|
||||
|
||||
$this->assertEquals(1, $domain1Pages->count());
|
||||
$this->assertEquals(1, $domain2Pages->count());
|
||||
$this->assertEquals('Domain 1 Seite', $domain1Pages->first()->getTranslation('title', 'de'));
|
||||
}
|
||||
|
||||
public function test_page_scope_by_slug()
|
||||
{
|
||||
$page = Page::create([
|
||||
'domain_key' => 'test',
|
||||
'title' => ['de' => 'Test Seite'],
|
||||
'slug' => ['de' => '/test-seite', 'en' => '/test-page'],
|
||||
]);
|
||||
|
||||
$foundPage = Page::bySlug('/test-seite', 'de')->first();
|
||||
$this->assertEquals($page->id, $foundPage->id);
|
||||
|
||||
$foundPage = Page::bySlug('/test-page', 'en')->first();
|
||||
$this->assertEquals($page->id, $foundPage->id);
|
||||
|
||||
$notFound = Page::bySlug('/not-found', 'de')->first();
|
||||
$this->assertNull($notFound);
|
||||
}
|
||||
|
||||
public function test_component_can_be_duplicated()
|
||||
{
|
||||
$page = Page::create([
|
||||
'domain_key' => 'test',
|
||||
'title' => ['de' => 'Test Seite'],
|
||||
'slug' => ['de' => '/test'],
|
||||
]);
|
||||
|
||||
$originalComponent = $page->allComponents()->create([
|
||||
'component_class' => 'TestComponent',
|
||||
'order' => 1,
|
||||
'content' => ['title' => ['de' => 'Original']],
|
||||
]);
|
||||
|
||||
$duplicate = $originalComponent->duplicate();
|
||||
|
||||
$this->assertEquals(2, $page->allComponents()->count());
|
||||
$this->assertEquals($originalComponent->content, $duplicate->content);
|
||||
$this->assertEquals(2, $duplicate->order);
|
||||
$this->assertNotEquals($originalComponent->id, $duplicate->id);
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app)
|
||||
{
|
||||
return [
|
||||
\FluxCms\Core\FluxCmsServiceProvider::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getEnvironmentSetUp($app)
|
||||
{
|
||||
$app['config']->set('database.default', 'testing');
|
||||
$app['config']->set('database.connections.testing', [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$app['config']->set('flux-cms.locales', [
|
||||
'de' => 'Deutsch',
|
||||
'en' => 'English',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Unit\Admin;
|
||||
|
||||
use FluxCms\Core\Models\User;
|
||||
use FluxCms\Core\Models\BlogPost;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
|
||||
class BlogControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $adminUser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->adminUser = User::factory()->create(['is_admin' => true]);
|
||||
$this->actingAs($this->adminUser);
|
||||
}
|
||||
|
||||
public function test_can_display_blog_posts_index()
|
||||
{
|
||||
BlogPost::factory()->count(3)->create();
|
||||
$response = $this->get(route('admin.cms.blog.index'));
|
||||
$response->assertOk();
|
||||
$response->assertViewHas('posts');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Unit\Admin;
|
||||
|
||||
use FluxCms\Core\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
|
||||
class DashboardControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $adminUser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->adminUser = User::factory()->create(['is_admin' => true]);
|
||||
$this->actingAs($this->adminUser);
|
||||
}
|
||||
|
||||
public function test_admin_can_see_dashboard()
|
||||
{
|
||||
$response = $this->get(route('admin.cms.index'));
|
||||
$response->assertOk();
|
||||
$response->assertViewHas('stats');
|
||||
$response->assertViewHas('recentPages');
|
||||
}
|
||||
}
|
||||
123
packages/flux-cms/core/tests/Unit/Admin/PageControllerTest.php
Normal file
123
packages/flux-cms/core/tests/Unit/Admin/PageControllerTest.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Unit\Admin;
|
||||
|
||||
use FluxCms\Core\Models\User;
|
||||
use FluxCms\Core\Models\Page;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
|
||||
class PageControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $adminUser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->adminUser = User::factory()->create(['is_admin' => true]);
|
||||
$this->actingAs($this->adminUser);
|
||||
}
|
||||
|
||||
public function test_can_display_pages_index()
|
||||
{
|
||||
Page::factory()->count(3)->create();
|
||||
$response = $this->get(route('admin.cms.pages.index'));
|
||||
$response->assertOk();
|
||||
$response->assertViewHas('pages');
|
||||
}
|
||||
|
||||
public function test_can_show_create_page_form()
|
||||
{
|
||||
$response = $this->get(route('admin.cms.pages.create'));
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_can_store_a_new_page()
|
||||
{
|
||||
$pageData = [
|
||||
'domain_key' => 'default',
|
||||
'title' => ['en' => 'New Page'],
|
||||
'slugs' => ['en' => '/new-page'],
|
||||
'is_published' => true,
|
||||
];
|
||||
|
||||
$response = $this->post(route('admin.cms.pages.store'), $pageData);
|
||||
|
||||
$this->assertDatabaseHas('flux_cms_pages', ['title' => '{"en":"New Page"}']);
|
||||
$this->assertDatabaseHas('flux_cms_slugs', ['slug' => '/new-page']);
|
||||
$response->assertRedirect();
|
||||
}
|
||||
|
||||
public function test_store_fails_with_invalid_data()
|
||||
{
|
||||
$response = $this->post(route('admin.cms.pages.store'), [
|
||||
'title' => ['en' => ''], // Invalid: title is required
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('title.en');
|
||||
$response->assertStatus(302); // Should redirect back
|
||||
}
|
||||
|
||||
public function test_unauthorized_user_cannot_create_page()
|
||||
{
|
||||
$user = User::factory()->create(); // Non-admin user
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.cms.pages.create'));
|
||||
$response->assertStatus(403);
|
||||
|
||||
$response = $this->post(route('admin.cms.pages.store'), []);
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_can_show_edit_page_form()
|
||||
{
|
||||
$page = Page::factory()->create();
|
||||
$response = $this->get(route('admin.cms.pages.edit', $page));
|
||||
$response->assertOk();
|
||||
$response->assertViewHas('page', $page);
|
||||
}
|
||||
|
||||
public function test_can_update_a_page()
|
||||
{
|
||||
$page = Page::factory()->create();
|
||||
$page->slugs()->create(['locale' => 'en', 'slug' => '/old-slug']);
|
||||
|
||||
$updateData = [
|
||||
'title' => ['en' => 'Updated Title'],
|
||||
'slugs' => ['en' => '/updated-slug'],
|
||||
];
|
||||
|
||||
$response = $this->put(route('admin.cms.pages.update', $page), $updateData);
|
||||
|
||||
$this->assertDatabaseHas('flux_cms_pages', ['id' => $page->id, 'title' => '{"en":"Updated Title"}']);
|
||||
$this->assertDatabaseHas('flux_cms_slugs', ['slug' => '/updated-slug']);
|
||||
$response->assertRedirect();
|
||||
}
|
||||
|
||||
public function test_unauthorized_user_cannot_edit_or_delete_page()
|
||||
{
|
||||
$user = User::factory()->create(); // Non-admin user
|
||||
$page = Page::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.cms.pages.edit', $page));
|
||||
$response->assertStatus(403);
|
||||
|
||||
$response = $this->put(route('admin.cms.pages.update', $page), []);
|
||||
$response->assertStatus(403);
|
||||
|
||||
$response = $this->delete(route('admin.cms.pages.destroy', $page));
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_can_delete_a_page()
|
||||
{
|
||||
$page = Page::factory()->create();
|
||||
$response = $this->delete(route('admin.cms.pages.destroy', $page));
|
||||
$this->assertModelMissing($page);
|
||||
$response->assertRedirect(route('admin.cms.pages.index'));
|
||||
}
|
||||
}
|
||||
165
packages/flux-cms/core/tests/Unit/ComponentRegistryTest.php
Normal file
165
packages/flux-cms/core/tests/Unit/ComponentRegistryTest.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Unit;
|
||||
|
||||
use FluxCms\Core\Services\ComponentRegistry;
|
||||
use FluxCms\Core\FieldTypes\TextField;
|
||||
use FluxCms\Core\FieldTypes\MediaField;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
use Livewire\Component;
|
||||
use Mockery;
|
||||
|
||||
class ComponentRegistryTest extends TestCase
|
||||
{
|
||||
protected ComponentRegistry $registry;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->registry = new ComponentRegistry();
|
||||
}
|
||||
|
||||
public function test_can_detect_valid_component()
|
||||
{
|
||||
$componentClass = TestComponent::class;
|
||||
|
||||
$this->assertTrue($this->registry->isValidComponent($componentClass));
|
||||
}
|
||||
|
||||
public function test_can_get_component_config()
|
||||
{
|
||||
$componentClass = TestComponent::class;
|
||||
|
||||
$config = $this->registry->getComponentConfig($componentClass);
|
||||
|
||||
$this->assertIsArray($config);
|
||||
$this->assertEquals('Test Component', $config['name']);
|
||||
$this->assertEquals('Testing', $config['category']);
|
||||
$this->assertCount(2, $config['fields']);
|
||||
}
|
||||
|
||||
public function test_can_validate_component_content()
|
||||
{
|
||||
$componentClass = TestComponent::class;
|
||||
|
||||
// Valid content
|
||||
$validContent = [
|
||||
'title' => ['de' => 'Test Titel', 'en' => 'Test Title'],
|
||||
'image' => 123
|
||||
];
|
||||
|
||||
$errors = $this->registry->validateComponentContent($componentClass, $validContent);
|
||||
$this->assertEmpty($errors);
|
||||
|
||||
// Invalid content (missing required field)
|
||||
$invalidContent = [
|
||||
'image' => 123
|
||||
];
|
||||
|
||||
$errors = $this->registry->validateComponentContent($componentClass, $invalidContent);
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
|
||||
public function test_can_search_components()
|
||||
{
|
||||
// Mock some components in registry
|
||||
$components = [
|
||||
TestComponent::class => [
|
||||
'name' => 'Test Component',
|
||||
'category' => 'Testing',
|
||||
'description' => 'A test component',
|
||||
'tags' => ['test', 'example']
|
||||
]
|
||||
];
|
||||
|
||||
$registry = Mockery::mock(ComponentRegistry::class)->makePartial();
|
||||
$registry->shouldReceive('getAvailableComponents')->andReturn($components);
|
||||
|
||||
$results = $registry->searchComponents('test');
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertArrayHasKey(TestComponent::class, $results);
|
||||
}
|
||||
|
||||
public function test_can_get_components_by_category()
|
||||
{
|
||||
$components = [
|
||||
TestComponent::class => [
|
||||
'name' => 'Test Component',
|
||||
'category' => 'Testing',
|
||||
],
|
||||
AnotherTestComponent::class => [
|
||||
'name' => 'Another Component',
|
||||
'category' => 'Layout',
|
||||
]
|
||||
];
|
||||
|
||||
$registry = Mockery::mock(ComponentRegistry::class)->makePartial();
|
||||
$registry->shouldReceive('getAvailableComponents')->andReturn($components);
|
||||
|
||||
$categorized = $registry->getComponentsByCategory();
|
||||
|
||||
$this->assertArrayHasKey('Testing', $categorized);
|
||||
$this->assertArrayHasKey('Layout', $categorized);
|
||||
$this->assertCount(1, $categorized['Testing']);
|
||||
$this->assertCount(1, $categorized['Layout']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
|
||||
// Test component classes
|
||||
class TestComponent extends Component
|
||||
{
|
||||
public static function getCmsName(): string
|
||||
{
|
||||
return 'Test Component';
|
||||
}
|
||||
|
||||
public static function getCmsCategory(): string
|
||||
{
|
||||
return 'Testing';
|
||||
}
|
||||
|
||||
public static function getCmsFields(): array
|
||||
{
|
||||
return [
|
||||
TextField::make('title', 'Title')->translatable()->required(),
|
||||
MediaField::make('image', 'Image')->images(),
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<div>Test Component</div>';
|
||||
}
|
||||
}
|
||||
|
||||
class AnotherTestComponent extends Component
|
||||
{
|
||||
public static function getCmsName(): string
|
||||
{
|
||||
return 'Another Component';
|
||||
}
|
||||
|
||||
public static function getCmsCategory(): string
|
||||
{
|
||||
return 'Layout';
|
||||
}
|
||||
|
||||
public static function getCmsFields(): array
|
||||
{
|
||||
return [
|
||||
TextField::make('content', 'Content'),
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<div>Another Component</div>';
|
||||
}
|
||||
}
|
||||
33
packages/flux-cms/core/tests/Unit/Models/BlogPostTest.php
Normal file
33
packages/flux-cms/core/tests/Unit/Models/BlogPostTest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Unit\Models;
|
||||
|
||||
use FluxCms\Core\Models\BlogPost;
|
||||
use FluxCms\Core\Models\User;
|
||||
use Spatie\Tags\Tag;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
|
||||
class BlogPostTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_blog_post_has_author_relationship()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$post = BlogPost::factory()->create([
|
||||
'author_id' => $user->id,
|
||||
'author_type' => User::class
|
||||
]);
|
||||
$this->assertInstanceOf(User::class, $post->author);
|
||||
}
|
||||
|
||||
public function test_can_attach_and_retrieve_tags()
|
||||
{
|
||||
$post = BlogPost::factory()->create();
|
||||
$post->attachTag('Laravel');
|
||||
|
||||
$this->assertCount(1, $post->tags);
|
||||
$this->assertEquals('Laravel', $post->tags->first()->name);
|
||||
}
|
||||
}
|
||||
49
packages/flux-cms/core/tests/Unit/Models/PageTest.php
Normal file
49
packages/flux-cms/core/tests/Unit/Models/PageTest.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace FluxCms\Core\Tests\Unit\Models;
|
||||
|
||||
use FluxCms\Core\Models\Page;
|
||||
use FluxCms\Core\Models\Slug;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
|
||||
class PageTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_page_has_slugs_relationship()
|
||||
{
|
||||
$page = Page::factory()->create();
|
||||
Slug::factory()->create([
|
||||
'model_id' => $page->id,
|
||||
'model_type' => Page::class
|
||||
]);
|
||||
$this->assertInstanceOf(Slug::class, $page->slugs->first());
|
||||
}
|
||||
|
||||
public function test_published_scope_returns_only_published_pages()
|
||||
{
|
||||
Page::factory()->create(['is_published' => true]);
|
||||
Page::factory()->create(['is_published' => false]);
|
||||
|
||||
$this->assertEquals(1, Page::published()->count());
|
||||
}
|
||||
|
||||
public function test_for_domain_scope_returns_pages_for_correct_domain()
|
||||
{
|
||||
Page::factory()->create(['domain_key' => 'domain-a']);
|
||||
Page::factory()->create(['domain_key' => 'domain-b']);
|
||||
|
||||
$this->assertEquals(1, Page::forDomain('domain-a')->count());
|
||||
}
|
||||
|
||||
public function test_by_slug_with_fallback_scope_finds_page_by_slug()
|
||||
{
|
||||
$page = Page::factory()->create();
|
||||
$page->slugs()->create(['locale' => 'en', 'slug' => 'test-slug']);
|
||||
|
||||
$foundPage = Page::bySlugWithFallback('test-slug', 'en')->first();
|
||||
$this->assertNotNull($foundPage);
|
||||
$this->assertEquals($page->id, $foundPage->id);
|
||||
}
|
||||
}
|
||||
22
packages/flux-cms/core/vendor/autoload.php
vendored
Normal file
22
packages/flux-cms/core/vendor/autoload.php
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit3b19c1bf4f41f1c6ad6362da08433039::getLoader();
|
||||
119
packages/flux-cms/core/vendor/bin/canvas
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/canvas
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../orchestra/canvas/canvas)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/orchestra/canvas/canvas');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/orchestra/canvas/canvas';
|
||||
119
packages/flux-cms/core/vendor/bin/carbon
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/carbon
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
||||
119
packages/flux-cms/core/vendor/bin/paratest
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/paratest
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../brianium/paratest/bin/paratest)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/brianium/paratest/bin/paratest');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/brianium/paratest/bin/paratest';
|
||||
119
packages/flux-cms/core/vendor/bin/paratest_for_phpstorm
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/paratest_for_phpstorm
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../brianium/paratest/bin/paratest_for_phpstorm)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/brianium/paratest/bin/paratest_for_phpstorm');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/brianium/paratest/bin/paratest_for_phpstorm';
|
||||
119
packages/flux-cms/core/vendor/bin/patch-type-declarations
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/patch-type-declarations
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';
|
||||
119
packages/flux-cms/core/vendor/bin/pest
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/pest
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../pestphp/pest/bin/pest)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/pestphp/pest/bin/pest');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/pestphp/pest/bin/pest';
|
||||
119
packages/flux-cms/core/vendor/bin/php-parse
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/php-parse
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
|
||||
122
packages/flux-cms/core/vendor/bin/phpunit
vendored
Executable file
122
packages/flux-cms/core/vendor/bin/phpunit
vendored
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = 'phpvfscomposer://'.$this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
||||
119
packages/flux-cms/core/vendor/bin/psysh
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/psysh
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
|
||||
119
packages/flux-cms/core/vendor/bin/testbench
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/testbench
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../orchestra/testbench-core/testbench)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/orchestra/testbench-core/testbench');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/orchestra/testbench-core/testbench';
|
||||
119
packages/flux-cms/core/vendor/bin/var-dump-server
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/var-dump-server
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
||||
119
packages/flux-cms/core/vendor/bin/yaml-lint
vendored
Executable file
119
packages/flux-cms/core/vendor/bin/yaml-lint
vendored
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';
|
||||
19
packages/flux-cms/core/vendor/brianium/paratest/LICENSE
vendored
Normal file
19
packages/flux-cms/core/vendor/brianium/paratest/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2013 Brian Scaturro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
157
packages/flux-cms/core/vendor/brianium/paratest/README.md
vendored
Normal file
157
packages/flux-cms/core/vendor/brianium/paratest/README.md
vendored
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
ParaTest
|
||||
========
|
||||
|
||||
[](https://packagist.org/packages/brianium/paratest)
|
||||
[](https://packagist.org/packages/brianium/paratest)
|
||||
[](https://github.com/paratestphp/paratest/actions)
|
||||
[](https://dashboard.stryker-mutator.io/reports/github.com/paratestphp/paratest/7.x)
|
||||
|
||||
The objective of ParaTest is to support parallel testing in PHPUnit. Provided you have well-written PHPUnit tests, you can drop `paratest` in your project and
|
||||
start using it with no additional bootstrap or configurations!
|
||||
|
||||
Benefits:
|
||||
|
||||
* Zero configuration. After the installation, run with `vendor/bin/paratest` to parallelize by TestCase or `vendor/bin/paratest --functional` to parallelize by Test. That's it!
|
||||
* Code Coverage report combining. Run your tests in N parallel processes and all the code coverage output will be combined into one report.
|
||||
|
||||
# Installation
|
||||
|
||||
To install with composer run the following command:
|
||||
|
||||
composer require --dev brianium/paratest
|
||||
|
||||
# Versions
|
||||
|
||||
Only the latest version of PHPUnit is supported, and thus only the latest version of ParaTest is actively maintained.
|
||||
|
||||
This is because of the following reasons:
|
||||
|
||||
1. To reduce bugs, code duplication and incompatibilities with PHPUnit, from version 5 ParaTest heavily relies on PHPUnit `@internal` classes
|
||||
1. The fast pace both PHP and PHPUnit have taken recently adds too much maintenance burden, which we can only afford for the latest versions to stay up-to-date
|
||||
|
||||
# Usage
|
||||
|
||||
After installation, the binary can be found at `vendor/bin/paratest`. Run it
|
||||
with `--help` option to see a complete list of the available options.
|
||||
|
||||
## Test token
|
||||
|
||||
The `TEST_TOKEN` environment variable is guaranteed to have a value that is different
|
||||
from every other currently running test. This is useful to e.g. use a different database
|
||||
for each test:
|
||||
|
||||
```php
|
||||
if (getenv('TEST_TOKEN') !== false) { // Using ParaTest
|
||||
$dbname = 'testdb_' . getenv('TEST_TOKEN');
|
||||
} else {
|
||||
$dbname = 'testdb';
|
||||
}
|
||||
```
|
||||
|
||||
A `UNIQUE_TEST_TOKEN` environment variable is also available and guaranteed to have a value that is unique both
|
||||
per run and per process.
|
||||
|
||||
## Code coverage
|
||||
|
||||
The cache is always warmed up by ParaTest before executing the test suite.
|
||||
|
||||
### PCOV
|
||||
|
||||
If you have installed `pcov` but need to enable it only while running tests, you have to pass thru the needed PHP binary
|
||||
option:
|
||||
|
||||
```
|
||||
php -d pcov.enabled=1 vendor/bin/paratest --passthru-php="'-d' 'pcov.enabled=1'"
|
||||
```
|
||||
|
||||
### xDebug
|
||||
|
||||
If you have `xDebug` installed, activating it by the environment variable is enough to have it running even in the subprocesses:
|
||||
|
||||
```
|
||||
XDEBUG_MODE=coverage vendor/bin/paratest
|
||||
```
|
||||
|
||||
## Initial setup for all tests
|
||||
|
||||
Because ParaTest runs multiple processes in parallel, each with their own instance of the PHP interpreter,
|
||||
techniques used to perform an initialization step exactly once for each test work different from PHPUnit.
|
||||
The following pattern will not work as expected - run the initialization exactly once - and instead run the
|
||||
initialization once per process:
|
||||
|
||||
```php
|
||||
private static bool $initialized = false;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
if (! self::$initialized) {
|
||||
self::initialize();
|
||||
self::$initialized = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is because static variables persist during the execution of a single process.
|
||||
In parallel testing each process has a separate instance of `$initialized`.
|
||||
You can use the following pattern to ensure your initialization runs exactly once for the entire test invocation:
|
||||
|
||||
```php
|
||||
static bool $initialized = false;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
if (! self::$initialized) {
|
||||
// We utilize the filesystem as shared mutable state to coordinate between processes
|
||||
touch('/tmp/test-initialization-lock-file');
|
||||
$lockFile = fopen('/tmp/test-initialization-lock-file', 'r');
|
||||
|
||||
// Attempt to get an exclusive lock - first process wins
|
||||
if (flock($lockFile, LOCK_EX | LOCK_NB)) {
|
||||
// Since we are the single process that has an exclusive lock, we run the initialization
|
||||
self::initialize();
|
||||
} else {
|
||||
// If no exclusive lock is available, block until the first process is done with initialization
|
||||
flock($lockFile, LOCK_SH);
|
||||
}
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you run into problems with `paratest`, try to get more information about the issue by enabling debug output via
|
||||
`--verbose --debug`.
|
||||
|
||||
When a sub-process fails, the originating command is given in the output and can then be copy-pasted in the terminal
|
||||
to be run and debugged. All internal commands run with `--printer [...]\NullPhpunitPrinter` which silence the original
|
||||
PHPUnit output: during a debugging run remove that option to restore the output and see what PHPUnit is doing.
|
||||
|
||||
## Caveats
|
||||
|
||||
1. Constants, static methods, static variables and everything exposed by test classes consumed by other test classes
|
||||
(including Reflection) are not supported. This is due to a limitation of the current implementation of `WrapperRunner`
|
||||
and how PHPUnit searches for classes. The fix is to put shared code into classes which are not tests _themselves_.
|
||||
|
||||
## Integration with PHPStorm
|
||||
|
||||
ParaTest provides a dedicated binary to work with PHPStorm; follow these steps to have ParaTest working within it:
|
||||
|
||||
1. Be sure you have PHPUnit already configured in PHPStorm: https://www.jetbrains.com/help/phpstorm/using-phpunit-framework.html#php_test_frameworks_phpunit_integrate
|
||||
2. Go to `Run` -> `Edit configurations...`
|
||||
3. Select `Add new Configuration`, select the `PHPUnit` type and name it `ParaTest`
|
||||
4. In the `Command Line` -> `Interpreter options` add `./vendor/bin/paratest_for_phpstorm`
|
||||
5. Any additional ParaTest options you want to pass to ParaTest should go within the `Test runner` -> `Test runner options` section
|
||||
|
||||
You should now have a `ParaTest` run within your configurations list.
|
||||
It should natively work with the `Rerun failed tests` and `Toggle auto-test` buttons of the `Run` overlay.
|
||||
|
||||
### Run with Coverage
|
||||
|
||||
Coverage with one of the [available coverage engines](#code-coverage) must already be [configured in PHPStorm](https://www.jetbrains.com/help/phpstorm/code-coverage.html)
|
||||
and working when running tests sequentially in order for the helper binary to correctly handle code coverage
|
||||
|
||||
# For Contributors: testing ParaTest itself
|
||||
|
||||
Before creating a Pull Request be sure to run all the necessary checks with `make` command.
|
||||
37
packages/flux-cms/core/vendor/brianium/paratest/bin/paratest
vendored
Executable file
37
packages/flux-cms/core/vendor/brianium/paratest/bin/paratest
vendored
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
$cwd = getcwd();
|
||||
|
||||
$files = array(
|
||||
dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'autoload.php',
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php',
|
||||
dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'
|
||||
);
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
die(
|
||||
'You need to set up the project dependencies using the following commands:' . PHP_EOL .
|
||||
'curl -s http://getcomposer.org/installer | php' . PHP_EOL .
|
||||
'php composer.phar install' . PHP_EOL
|
||||
);
|
||||
}
|
||||
|
||||
if (false === in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
|
||||
echo PHP_EOL . 'ParaTest may only be invoked from a command line, got "' . PHP_SAPI . '"' . PHP_EOL;
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
assert(is_string($cwd) && '' !== $cwd);
|
||||
ParaTest\ParaTestCommand::applicationFactory($cwd)->run();
|
||||
10
packages/flux-cms/core/vendor/brianium/paratest/bin/paratest_for_phpstorm
vendored
Executable file
10
packages/flux-cms/core/vendor/brianium/paratest/bin/paratest_for_phpstorm
vendored
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use ParaTest\Util\PhpstormHelper;
|
||||
|
||||
require dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Util' . DIRECTORY_SEPARATOR . 'PhpstormHelper.php';
|
||||
|
||||
require PhpstormHelper::handleArgvFromPhpstorm($_SERVER['argv'], __DIR__ . '/paratest');
|
||||
85
packages/flux-cms/core/vendor/brianium/paratest/bin/phpunit-wrapper.php
vendored
Normal file
85
packages/flux-cms/core/vendor/brianium/paratest/bin/phpunit-wrapper.php
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use ParaTest\WrapperRunner\ApplicationForWrapperWorker;
|
||||
use ParaTest\WrapperRunner\WrapperWorker;
|
||||
|
||||
(static function (): void {
|
||||
$getopt = getopt('', [
|
||||
'status-file:',
|
||||
'progress-file:',
|
||||
'unexpected-output-file:',
|
||||
'test-result-file:',
|
||||
'result-cache-file:',
|
||||
'teamcity-file:',
|
||||
'testdox-file:',
|
||||
'testdox-color',
|
||||
'testdox-columns:',
|
||||
'testdox-summary',
|
||||
'phpunit-argv:',
|
||||
]);
|
||||
|
||||
$composerAutoloadFiles = [
|
||||
dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'autoload.php',
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php',
|
||||
dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php',
|
||||
];
|
||||
|
||||
foreach ($composerAutoloadFiles as $file) {
|
||||
if (file_exists($file)) {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', $file);
|
||||
require_once $file;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert(isset($getopt['status-file']) && is_string($getopt['status-file']));
|
||||
$statusFile = fopen($getopt['status-file'], 'wb');
|
||||
assert(is_resource($statusFile));
|
||||
|
||||
assert(isset($getopt['progress-file']) && is_string($getopt['progress-file']));
|
||||
assert(isset($getopt['unexpected-output-file']) && is_string($getopt['unexpected-output-file']));
|
||||
assert(isset($getopt['test-result-file']) && is_string($getopt['test-result-file']));
|
||||
assert(!isset($getopt['result-cache-file']) || is_string($getopt['result-cache-file']));
|
||||
assert(!isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
|
||||
assert(!isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
|
||||
assert(!isset($getopt['testdox-columns']) || $getopt['testdox-columns'] === (string) (int) $getopt['testdox-columns']);
|
||||
|
||||
assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv']));
|
||||
$phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]);
|
||||
assert(is_array($phpunitArgv));
|
||||
|
||||
$application = new ApplicationForWrapperWorker(
|
||||
$phpunitArgv,
|
||||
$getopt['progress-file'],
|
||||
$getopt['unexpected-output-file'],
|
||||
$getopt['test-result-file'],
|
||||
$getopt['result-cache-file'] ?? null,
|
||||
$getopt['teamcity-file'] ?? null,
|
||||
$getopt['testdox-file'] ?? null,
|
||||
isset($getopt['testdox-color']),
|
||||
isset($getopt['testdox-columns']) ? (int) $getopt['testdox-columns'] : null,
|
||||
isset($getopt['testdox-summary']),
|
||||
);
|
||||
|
||||
while (true) {
|
||||
if (feof(STDIN)) {
|
||||
$application->end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$testPath = fgets(STDIN);
|
||||
if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) {
|
||||
$application->end();
|
||||
exit;
|
||||
}
|
||||
|
||||
// It must be a 1 byte string to ensure filesize() is equal to the number of tests executed
|
||||
$exitCode = $application->runTest(trim($testPath, "\n"));
|
||||
|
||||
fwrite($statusFile, (string) $exitCode);
|
||||
fflush($statusFile);
|
||||
}
|
||||
})();
|
||||
86
packages/flux-cms/core/vendor/brianium/paratest/composer.json
vendored
Normal file
86
packages/flux-cms/core/vendor/brianium/paratest/composer.json
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"name": "brianium/paratest",
|
||||
"description": "Parallel testing for PHP",
|
||||
"license": "MIT",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"testing",
|
||||
"PHPUnit",
|
||||
"concurrent",
|
||||
"parallel"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brian Scaturro",
|
||||
"email": "scaturrob@gmail.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Filippo Tessarotto",
|
||||
"email": "zoeslam@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"homepage": "https://github.com/paratestphp/paratest",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Slamdunk"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/filippotessarotto"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
|
||||
"ext-dom": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-reflection": "*",
|
||||
"ext-simplexml": "*",
|
||||
"fidry/cpu-core-counter": "^1.2.0",
|
||||
"jean85/pretty-package-versions": "^2.1.1",
|
||||
"phpunit/php-code-coverage": "^11.0.10",
|
||||
"phpunit/php-file-iterator": "^5.1.0",
|
||||
"phpunit/php-timer": "^7.0.1",
|
||||
"phpunit/phpunit": "^11.5.24",
|
||||
"sebastian/environment": "^7.2.1",
|
||||
"symfony/console": "^6.4.22 || ^7.3.0",
|
||||
"symfony/process": "^6.4.20 || ^7.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-pcov": "*",
|
||||
"ext-posix": "*",
|
||||
"doctrine/coding-standard": "^12.0.0",
|
||||
"phpstan/phpstan": "^2.1.17",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.6",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.4",
|
||||
"squizlabs/php_codesniffer": "^3.13.2",
|
||||
"symfony/filesystem": "^6.4.13 || ^7.3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ParaTest\\": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"ParaTest\\Tests\\": "test/"
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/paratest",
|
||||
"bin/paratest_for_phpstorm"
|
||||
],
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"infection/extension-installer": true
|
||||
},
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
6
packages/flux-cms/core/vendor/brianium/paratest/renovate.json
vendored
Normal file
6
packages/flux-cms/core/vendor/brianium/paratest/renovate.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>Slamdunk/.github:renovate-config"
|
||||
]
|
||||
}
|
||||
32
packages/flux-cms/core/vendor/brianium/paratest/src/Coverage/CoverageMerger.php
vendored
Normal file
32
packages/flux-cms/core/vendor/brianium/paratest/src/Coverage/CoverageMerger.php
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\Coverage;
|
||||
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SplFileInfo;
|
||||
|
||||
use function assert;
|
||||
|
||||
/** @internal */
|
||||
final readonly class CoverageMerger
|
||||
{
|
||||
public function __construct(
|
||||
private CodeCoverage $coverage
|
||||
) {
|
||||
}
|
||||
|
||||
public function addCoverageFromFile(SplFileInfo $coverageFile): void
|
||||
{
|
||||
if (! $coverageFile->isFile() || $coverageFile->getSize() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @psalm-suppress UnresolvableInclude **/
|
||||
$coverage = include $coverageFile->getPathname();
|
||||
assert($coverage instanceof CodeCoverage);
|
||||
|
||||
$this->coverage->merge($coverage);
|
||||
}
|
||||
}
|
||||
105
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/LogMerger.php
vendored
Normal file
105
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/LogMerger.php
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use SplFileInfo;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function ksort;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final readonly class LogMerger
|
||||
{
|
||||
/** @param list<SplFileInfo> $junitFiles */
|
||||
public function merge(array $junitFiles): ?TestSuite
|
||||
{
|
||||
$mainSuite = null;
|
||||
foreach ($junitFiles as $junitFile) {
|
||||
if (! $junitFile->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$otherSuite = TestSuite::fromFile($junitFile);
|
||||
if ($mainSuite === null) {
|
||||
$mainSuite = $otherSuite;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($mainSuite->name !== $otherSuite->name) {
|
||||
if ($mainSuite->name !== '') {
|
||||
$mainSuite = new TestSuite(
|
||||
'',
|
||||
$mainSuite->tests,
|
||||
$mainSuite->assertions,
|
||||
$mainSuite->failures,
|
||||
$mainSuite->errors,
|
||||
$mainSuite->skipped,
|
||||
$mainSuite->time,
|
||||
'',
|
||||
[$mainSuite->name => $mainSuite],
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
if ($otherSuite->name !== '') {
|
||||
$otherSuite = new TestSuite(
|
||||
'',
|
||||
$otherSuite->tests,
|
||||
$otherSuite->assertions,
|
||||
$otherSuite->failures,
|
||||
$otherSuite->errors,
|
||||
$otherSuite->skipped,
|
||||
$otherSuite->time,
|
||||
'',
|
||||
[$otherSuite->name => $otherSuite],
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$mainSuite = $this->mergeSuites($mainSuite, $otherSuite);
|
||||
}
|
||||
|
||||
return $mainSuite;
|
||||
}
|
||||
|
||||
private function mergeSuites(TestSuite $suite1, TestSuite $suite2): TestSuite
|
||||
{
|
||||
assert($suite1->name === $suite2->name);
|
||||
|
||||
$suites = $suite1->suites;
|
||||
foreach ($suite2->suites as $suite2suiteName => $suite2suite) {
|
||||
if (! isset($suites[$suite2suiteName])) {
|
||||
$suites[$suite2suiteName] = $suite2suite;
|
||||
continue;
|
||||
}
|
||||
|
||||
$suites[$suite2suiteName] = $this->mergeSuites(
|
||||
$suites[$suite2suiteName],
|
||||
$suite2suite,
|
||||
);
|
||||
}
|
||||
|
||||
ksort($suites);
|
||||
|
||||
return new TestSuite(
|
||||
$suite1->name,
|
||||
$suite1->tests + $suite2->tests,
|
||||
$suite1->assertions + $suite2->assertions,
|
||||
$suite1->failures + $suite2->failures,
|
||||
$suite1->errors + $suite2->errors,
|
||||
$suite1->skipped + $suite2->skipped,
|
||||
$suite1->time + $suite2->time,
|
||||
$suite1->file,
|
||||
$suites,
|
||||
array_merge($suite1->cases, $suite2->cases),
|
||||
);
|
||||
}
|
||||
}
|
||||
22
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/MessageType.php
vendored
Normal file
22
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/MessageType.php
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
/** @internal */
|
||||
enum MessageType
|
||||
{
|
||||
case error;
|
||||
case failure;
|
||||
case skipped;
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::error => 'error',
|
||||
self::failure => 'failure',
|
||||
self::skipped => 'skipped',
|
||||
};
|
||||
}
|
||||
}
|
||||
120
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/TestCase.php
vendored
Normal file
120
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/TestCase.php
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function current;
|
||||
use function iterator_to_array;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
readonly class TestCase
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $class,
|
||||
public string $file,
|
||||
public int $line,
|
||||
public int $assertions,
|
||||
public float $time
|
||||
) {
|
||||
}
|
||||
|
||||
final public static function caseFromNode(SimpleXMLElement $node): self
|
||||
{
|
||||
$getFirstNode = static function (array $nodes): SimpleXMLElement {
|
||||
assert(count($nodes) === 1);
|
||||
$node = current($nodes);
|
||||
assert($node instanceof SimpleXMLElement);
|
||||
|
||||
return $node;
|
||||
};
|
||||
$getType = static function (SimpleXMLElement $node): string {
|
||||
$element = $node->attributes();
|
||||
assert($element !== null);
|
||||
$attributes = iterator_to_array($element);
|
||||
assert($attributes !== []);
|
||||
|
||||
return (string) $attributes['type'];
|
||||
};
|
||||
|
||||
if (($errors = $node->xpath('error')) !== []) {
|
||||
$error = $getFirstNode($errors);
|
||||
$type = $getType($error);
|
||||
$text = (string) $error;
|
||||
|
||||
return new TestCaseWithMessage(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
$type,
|
||||
$text,
|
||||
MessageType::error,
|
||||
);
|
||||
}
|
||||
|
||||
if (($failures = $node->xpath('failure')) !== []) {
|
||||
$failure = $getFirstNode($failures);
|
||||
$type = $getType($failure);
|
||||
$text = (string) $failure;
|
||||
|
||||
return new TestCaseWithMessage(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
$type,
|
||||
$text,
|
||||
MessageType::failure,
|
||||
);
|
||||
}
|
||||
|
||||
if ($node->xpath('skipped') !== []) {
|
||||
$text = (string) $node['name'];
|
||||
if ((string) $node['class'] !== '') {
|
||||
$text = sprintf(
|
||||
"%s::%s\n\n%s:%s",
|
||||
$node['class'],
|
||||
$node['name'],
|
||||
$node['file'],
|
||||
$node['line'],
|
||||
);
|
||||
}
|
||||
|
||||
return new TestCaseWithMessage(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
null,
|
||||
$text,
|
||||
MessageType::skipped,
|
||||
);
|
||||
}
|
||||
|
||||
return new self(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
);
|
||||
}
|
||||
}
|
||||
27
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/TestCaseWithMessage.php
vendored
Normal file
27
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/TestCaseWithMessage.php
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final readonly class TestCaseWithMessage extends TestCase
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $class,
|
||||
string $file,
|
||||
int $line,
|
||||
int $assertions,
|
||||
float $time,
|
||||
public ?string $type,
|
||||
public string $text,
|
||||
public MessageType $xmlTagName
|
||||
) {
|
||||
parent::__construct($name, $class, $file, $line, $assertions, $time);
|
||||
}
|
||||
}
|
||||
110
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/TestSuite.php
vendored
Normal file
110
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/TestSuite.php
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use SimpleXMLElement;
|
||||
use SplFileInfo;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function file_get_contents;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final readonly class TestSuite
|
||||
{
|
||||
/**
|
||||
* @param array<string, TestSuite> $suites
|
||||
* @param list<TestCase> $cases
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public int $tests,
|
||||
public int $assertions,
|
||||
public int $failures,
|
||||
public int $errors,
|
||||
public int $skipped,
|
||||
public float $time,
|
||||
public string $file,
|
||||
public array $suites,
|
||||
public array $cases
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromFile(SplFileInfo $logFile): self
|
||||
{
|
||||
assert($logFile->isFile() && 0 < (int) $logFile->getSize());
|
||||
|
||||
$logFileContents = file_get_contents($logFile->getPathname());
|
||||
assert($logFileContents !== false);
|
||||
|
||||
return self::parseTestSuite(
|
||||
new SimpleXMLElement($logFileContents),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private static function parseTestSuite(SimpleXMLElement $node, bool $isRootSuite): self
|
||||
{
|
||||
if ($isRootSuite) {
|
||||
$tests = 0;
|
||||
$assertions = 0;
|
||||
$failures = 0;
|
||||
$errors = 0;
|
||||
$skipped = 0;
|
||||
$time = 0;
|
||||
} else {
|
||||
$tests = (int) $node['tests'];
|
||||
$assertions = (int) $node['assertions'];
|
||||
$failures = (int) $node['failures'];
|
||||
$errors = (int) $node['errors'];
|
||||
$skipped = (int) $node['skipped'];
|
||||
$time = (float) $node['time'];
|
||||
}
|
||||
|
||||
$count = count($node->testsuite);
|
||||
$suites = [];
|
||||
foreach ($node->testsuite as $singleTestSuiteXml) {
|
||||
$testSuite = self::parseTestSuite($singleTestSuiteXml, false);
|
||||
if ($isRootSuite && $count === 1) {
|
||||
return $testSuite;
|
||||
}
|
||||
|
||||
$suites[$testSuite->name] = $testSuite;
|
||||
|
||||
if (! $isRootSuite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tests += $testSuite->tests;
|
||||
$assertions += $testSuite->assertions;
|
||||
$failures += $testSuite->failures;
|
||||
$errors += $testSuite->errors;
|
||||
$skipped += $testSuite->skipped;
|
||||
$time += $testSuite->time;
|
||||
}
|
||||
|
||||
$cases = [];
|
||||
foreach ($node->testcase as $singleTestCase) {
|
||||
$cases[] = TestCase::caseFromNode($singleTestCase);
|
||||
}
|
||||
|
||||
return new self(
|
||||
(string) $node['name'],
|
||||
$tests,
|
||||
$assertions,
|
||||
$failures,
|
||||
$errors,
|
||||
$skipped,
|
||||
$time,
|
||||
(string) $node['file'],
|
||||
$suites,
|
||||
$cases,
|
||||
);
|
||||
}
|
||||
}
|
||||
114
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/Writer.php
vendored
Normal file
114
packages/flux-cms/core/vendor/brianium/paratest/src/JUnit/Writer.php
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
use function assert;
|
||||
use function dirname;
|
||||
use function file_put_contents;
|
||||
use function htmlspecialchars;
|
||||
use function is_dir;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
use function mkdir;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
|
||||
use const ENT_XML1;
|
||||
|
||||
/** @internal */
|
||||
final readonly class Writer
|
||||
{
|
||||
private const TESTSUITES_NAME = 'PHPUnit tests';
|
||||
private DOMDocument $document;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->document = new DOMDocument('1.0', 'UTF-8');
|
||||
$this->document->formatOutput = true;
|
||||
}
|
||||
|
||||
public function write(TestSuite $testSuite, string $path): void
|
||||
{
|
||||
$dir = dirname($path);
|
||||
if (! is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$result = file_put_contents($path, $this->getXml($testSuite));
|
||||
assert(is_int($result) && 0 < $result);
|
||||
}
|
||||
|
||||
/** @return non-empty-string */
|
||||
private function getXml(TestSuite $testSuite): string
|
||||
{
|
||||
$xmlTestsuites = $this->document->createElement('testsuites');
|
||||
$xmlTestsuites->appendChild($this->createSuiteNode($testSuite));
|
||||
$xmlTestsuites->setAttribute('name', self::TESTSUITES_NAME);
|
||||
$this->document->appendChild($xmlTestsuites);
|
||||
|
||||
$xml = $this->document->saveXML();
|
||||
assert(is_string($xml) && $xml !== '');
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function createSuiteNode(TestSuite $parentSuite): DOMElement
|
||||
{
|
||||
$suiteNode = $this->document->createElement('testsuite');
|
||||
$suiteNode->setAttribute('name', $parentSuite->name !== self::TESTSUITES_NAME ? $parentSuite->name : '');
|
||||
if ($parentSuite->file !== '') {
|
||||
$suiteNode->setAttribute('file', $parentSuite->file);
|
||||
}
|
||||
|
||||
$suiteNode->setAttribute('tests', (string) $parentSuite->tests);
|
||||
$suiteNode->setAttribute('assertions', (string) $parentSuite->assertions);
|
||||
$suiteNode->setAttribute('errors', (string) $parentSuite->errors);
|
||||
$suiteNode->setAttribute('failures', (string) $parentSuite->failures);
|
||||
$suiteNode->setAttribute('skipped', (string) $parentSuite->skipped);
|
||||
$suiteNode->setAttribute('time', (string) $parentSuite->time);
|
||||
|
||||
foreach ($parentSuite->suites as $suite) {
|
||||
$suiteNode->appendChild($this->createSuiteNode($suite));
|
||||
}
|
||||
|
||||
foreach ($parentSuite->cases as $case) {
|
||||
$suiteNode->appendChild($this->createCaseNode($case));
|
||||
}
|
||||
|
||||
return $suiteNode;
|
||||
}
|
||||
|
||||
private function createCaseNode(TestCase $case): DOMElement
|
||||
{
|
||||
$caseNode = $this->document->createElement('testcase');
|
||||
|
||||
$caseNode->setAttribute('name', $case->name);
|
||||
$caseNode->setAttribute('class', $case->class);
|
||||
$caseNode->setAttribute('classname', str_replace('\\', '.', $case->class));
|
||||
$caseNode->setAttribute('file', $case->file);
|
||||
$caseNode->setAttribute('line', (string) $case->line);
|
||||
$caseNode->setAttribute('assertions', (string) $case->assertions);
|
||||
$caseNode->setAttribute('time', sprintf('%F', $case->time));
|
||||
|
||||
if ($case instanceof TestCaseWithMessage) {
|
||||
if ($case->xmlTagName === MessageType::skipped) {
|
||||
$defectNode = $this->document->createElement($case->xmlTagName->toString());
|
||||
} else {
|
||||
$defectNode = $this->document->createElement($case->xmlTagName->toString(), htmlspecialchars($case->text, ENT_XML1));
|
||||
$type = $case->type;
|
||||
if ($type !== null) {
|
||||
$defectNode->setAttribute('type', $type);
|
||||
}
|
||||
}
|
||||
|
||||
$caseNode->appendChild($defectNode);
|
||||
}
|
||||
|
||||
return $caseNode;
|
||||
}
|
||||
}
|
||||
643
packages/flux-cms/core/vendor/brianium/paratest/src/Options.php
vendored
Normal file
643
packages/flux-cms/core/vendor/brianium/paratest/src/Options.php
vendored
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest;
|
||||
|
||||
use Fidry\CpuCoreCounter\CpuCoreCounter;
|
||||
use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound;
|
||||
use PHPUnit\TextUI\Configuration\Builder;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
use function array_filter;
|
||||
use function array_intersect_key;
|
||||
use function array_key_exists;
|
||||
use function array_shift;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function escapeshellarg;
|
||||
use function file_exists;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_numeric;
|
||||
use function is_string;
|
||||
use function realpath;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function sys_get_temp_dir;
|
||||
use function uniqid;
|
||||
use function unserialize;
|
||||
|
||||
use const PHP_BINARY;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final readonly class Options
|
||||
{
|
||||
public const ENV_KEY_TOKEN = 'TEST_TOKEN';
|
||||
public const ENV_KEY_UNIQUE_TOKEN = 'UNIQUE_TEST_TOKEN';
|
||||
|
||||
private const OPTIONS_TO_KEEP_FOR_PHPUNIT_IN_WORKER = [
|
||||
'bootstrap' => true,
|
||||
'cache-directory' => true,
|
||||
'configuration' => true,
|
||||
'coverage-filter' => true,
|
||||
'dont-report-useless-tests' => true,
|
||||
'exclude-group' => true,
|
||||
'fail-on-incomplete' => true,
|
||||
'fail-on-risky' => true,
|
||||
'fail-on-skipped' => true,
|
||||
'fail-on-warning' => true,
|
||||
'fail-on-deprecation' => true,
|
||||
'filter' => true,
|
||||
'group' => true,
|
||||
'no-configuration' => true,
|
||||
'order-by' => true,
|
||||
'process-isolation' => true,
|
||||
'random-order-seed' => true,
|
||||
'stop-on-defect' => true,
|
||||
'stop-on-error' => true,
|
||||
'stop-on-warning' => true,
|
||||
'stop-on-risky' => true,
|
||||
'stop-on-skipped' => true,
|
||||
'stop-on-incomplete' => true,
|
||||
'strict-coverage' => true,
|
||||
'strict-global-state' => true,
|
||||
'disallow-test-output' => true,
|
||||
];
|
||||
|
||||
public readonly bool $needsTeamcity;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $phpunit
|
||||
* @param non-empty-string $cwd
|
||||
* @param list<non-empty-string>|null $passthruPhp
|
||||
* @param array<non-empty-string, non-empty-string|true|list<non-empty-string>> $phpunitOptions
|
||||
* @param non-empty-string $runner
|
||||
* @param non-empty-string $tmpDir
|
||||
*/
|
||||
public function __construct(
|
||||
public Configuration $configuration,
|
||||
public string $phpunit,
|
||||
public string $cwd,
|
||||
public int $maxBatchSize,
|
||||
public bool $noTestTokens,
|
||||
public ?array $passthruPhp,
|
||||
public array $phpunitOptions,
|
||||
public int $processes,
|
||||
public string $runner,
|
||||
public string $tmpDir,
|
||||
public bool $verbose,
|
||||
public bool $functional,
|
||||
) {
|
||||
$this->needsTeamcity = $configuration->outputIsTeamCity() || $configuration->hasLogfileTeamcity();
|
||||
}
|
||||
|
||||
/** @param non-empty-string $cwd */
|
||||
public static function fromConsoleInput(InputInterface $input, string $cwd): self
|
||||
{
|
||||
$options = $input->getOptions();
|
||||
|
||||
$maxBatchSize = (int) $options['max-batch-size'];
|
||||
unset($options['max-batch-size']);
|
||||
|
||||
assert(is_bool($options['no-test-tokens']));
|
||||
$noTestTokens = $options['no-test-tokens'];
|
||||
unset($options['no-test-tokens']);
|
||||
|
||||
assert($options['passthru-php'] === null || is_string($options['passthru-php']));
|
||||
$passthruPhp = self::parsePassthru($options['passthru-php']);
|
||||
unset($options['passthru-php']);
|
||||
|
||||
assert(is_string($options['processes']));
|
||||
$processes = is_numeric($options['processes'])
|
||||
? (int) $options['processes']
|
||||
: self::getNumberOfCPUCores();
|
||||
unset($options['processes']);
|
||||
|
||||
assert(is_string($options['runner']) && $options['runner'] !== '');
|
||||
$runner = $options['runner'];
|
||||
unset($options['runner']);
|
||||
|
||||
assert(is_string($options['tmp-dir']) && $options['tmp-dir'] !== '');
|
||||
$tmpDir = $options['tmp-dir'];
|
||||
unset($options['tmp-dir']);
|
||||
|
||||
assert(is_bool($options['verbose']));
|
||||
$verbose = $options['verbose'];
|
||||
unset($options['verbose']);
|
||||
|
||||
assert(is_bool($options['functional']));
|
||||
$functional = $options['functional'];
|
||||
unset($options['functional']);
|
||||
|
||||
assert(array_key_exists('colors', $options));
|
||||
if ($options['colors'] === Configuration::COLOR_DEFAULT) {
|
||||
unset($options['colors']);
|
||||
} elseif ($options['colors'] === null) {
|
||||
$options['colors'] = Configuration::COLOR_AUTO;
|
||||
}
|
||||
|
||||
assert(array_key_exists('coverage-text', $options));
|
||||
if ($options['coverage-text'] === null) {
|
||||
$options['coverage-text'] = 'php://stdout';
|
||||
}
|
||||
|
||||
// Must be a static non-customizable reference because ParaTest code
|
||||
// is strictly coupled with PHPUnit pinned version
|
||||
$phpunit = self::getPhpunitBinary();
|
||||
if (str_starts_with($phpunit, $cwd)) {
|
||||
$phpunit = substr($phpunit, 1 + strlen($cwd));
|
||||
}
|
||||
|
||||
$phpunitArgv = [$phpunit];
|
||||
foreach ($options as $key => $value) {
|
||||
if ($value === null || $value === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === true) {
|
||||
$phpunitArgv[] = "--{$key}";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
foreach ($value as $innerValue) {
|
||||
$phpunitArgv[] = "--{$key}={$innerValue}";
|
||||
}
|
||||
}
|
||||
|
||||
if (($path = $input->getArgument('path')) !== null) {
|
||||
assert(is_string($path));
|
||||
$phpunitArgv[] = '--';
|
||||
$phpunitArgv[] = $path;
|
||||
}
|
||||
|
||||
$phpunitOptions = array_intersect_key($options, self::OPTIONS_TO_KEEP_FOR_PHPUNIT_IN_WORKER);
|
||||
$phpunitOptions = array_filter($phpunitOptions);
|
||||
|
||||
$configuration = (new Builder())->build($phpunitArgv);
|
||||
|
||||
return new self(
|
||||
$configuration,
|
||||
$phpunit,
|
||||
$cwd,
|
||||
$maxBatchSize,
|
||||
$noTestTokens,
|
||||
$passthruPhp,
|
||||
$phpunitOptions,
|
||||
$processes,
|
||||
$runner,
|
||||
$tmpDir,
|
||||
$verbose,
|
||||
$functional,
|
||||
);
|
||||
}
|
||||
|
||||
public static function setInputDefinition(InputDefinition $inputDefinition): void
|
||||
{
|
||||
$inputDefinition->setDefinition([
|
||||
// Arguments
|
||||
new InputArgument(
|
||||
'path',
|
||||
InputArgument::OPTIONAL,
|
||||
'The path to a directory or file containing tests.',
|
||||
),
|
||||
|
||||
// ParaTest options
|
||||
new InputOption(
|
||||
'functional',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Whether to enable functional testing, for unit and dataset parallelization',
|
||||
),
|
||||
new InputOption(
|
||||
'max-batch-size',
|
||||
'm',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Max batch size.',
|
||||
'0',
|
||||
),
|
||||
new InputOption(
|
||||
'no-test-tokens',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Disable TEST_TOKEN environment variables.',
|
||||
),
|
||||
new InputOption(
|
||||
'passthru-php',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Pass the given arguments verbatim to the underlying php process. Example: --passthru-php="\'-d\' ' .
|
||||
'\'pcov.enabled=1\'"',
|
||||
),
|
||||
new InputOption(
|
||||
'processes',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The number of test processes to run.',
|
||||
'auto',
|
||||
),
|
||||
new InputOption(
|
||||
'runner',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
sprintf('A %s.', RunnerInterface::class),
|
||||
'WrapperRunner',
|
||||
),
|
||||
new InputOption(
|
||||
'tmp-dir',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Temporary directory for internal ParaTest files',
|
||||
sys_get_temp_dir(),
|
||||
),
|
||||
new InputOption(
|
||||
'verbose',
|
||||
'v',
|
||||
InputOption::VALUE_NONE,
|
||||
'Output more verbose information',
|
||||
),
|
||||
|
||||
// PHPUnit options
|
||||
new InputOption(
|
||||
'bootstrap',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter = 'Configuration',
|
||||
),
|
||||
new InputOption(
|
||||
'configuration',
|
||||
'c',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'no-configuration',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'cache-directory',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'testsuite',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter = 'Selection',
|
||||
),
|
||||
new InputOption(
|
||||
'exclude-testsuite',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'group',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'exclude-group',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'filter',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'process-isolation',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter = 'Execution',
|
||||
),
|
||||
new InputOption(
|
||||
'strict-coverage',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'strict-global-state',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'disallow-test-output',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'dont-report-useless-tests',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-defect',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-error',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-failure',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-warning',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-risky',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-skipped',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'stop-on-incomplete',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'fail-on-incomplete',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'fail-on-risky',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'fail-on-skipped',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'fail-on-warning',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'fail-on-deprecation',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'order-by',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'random-order-seed',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'colors',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter = 'Reporting',
|
||||
Configuration::COLOR_DEFAULT,
|
||||
),
|
||||
new InputOption(
|
||||
'no-progress',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'display-incomplete',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'display-skipped',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'display-deprecations',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'display-errors',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'display-notices',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'display-warnings',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'teamcity',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'testdox',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'log-junit',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter = 'Logging',
|
||||
),
|
||||
new InputOption(
|
||||
'log-teamcity',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-clover',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter = 'Code Coverage',
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-cobertura',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-crap4j',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-html',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-php',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-text',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
false,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-xml',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'coverage-filter',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
new InputOption(
|
||||
'no-coverage',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'@see PHPUnit guide, chapter: ' . $chapter,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return non-empty-string $phpunit the path to phpunit */
|
||||
private static function getPhpunitBinary(): string
|
||||
{
|
||||
$tryPaths = [
|
||||
dirname(__DIR__, 3) . '/bin/phpunit',
|
||||
dirname(__DIR__, 3) . '/phpunit/phpunit/phpunit',
|
||||
dirname(__DIR__) . '/vendor/phpunit/phpunit/phpunit',
|
||||
];
|
||||
|
||||
foreach ($tryPaths as $path) {
|
||||
if (($realPath = realpath($path)) !== false && file_exists($realPath)) {
|
||||
return $realPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('PHPUnit not found'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
public static function getNumberOfCPUCores(): int
|
||||
{
|
||||
try {
|
||||
return (new CpuCoreCounter())->getCount();
|
||||
} catch (NumberOfCpuCoreNotFound) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<non-empty-string>|null */
|
||||
private static function parsePassthru(?string $param): ?array
|
||||
{
|
||||
if ($param === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stringToArgumentProcess = Process::fromShellCommandline(
|
||||
sprintf(
|
||||
'%s -r %s -- %s',
|
||||
escapeshellarg(PHP_BINARY),
|
||||
escapeshellarg('echo serialize($argv);'),
|
||||
$param,
|
||||
),
|
||||
);
|
||||
$stringToArgumentProcess->mustRun();
|
||||
|
||||
$passthruAsArguments = unserialize($stringToArgumentProcess->getOutput());
|
||||
assert(is_array($passthruAsArguments));
|
||||
array_shift($passthruAsArguments);
|
||||
|
||||
if (count($passthruAsArguments) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $passthruAsArguments;
|
||||
}
|
||||
|
||||
/** @return array{PARATEST: int, TEST_TOKEN?: int, UNIQUE_TEST_TOKEN?: non-empty-string} */
|
||||
public function fillEnvWithTokens(int $inc): array
|
||||
{
|
||||
$env = ['PARATEST' => 1];
|
||||
if (! $this->noTestTokens) {
|
||||
$env[self::ENV_KEY_TOKEN] = $inc;
|
||||
$env[self::ENV_KEY_UNIQUE_TOKEN] = uniqid($inc . '_');
|
||||
}
|
||||
|
||||
return $env;
|
||||
}
|
||||
}
|
||||
119
packages/flux-cms/core/vendor/brianium/paratest/src/ParaTestCommand.php
vendored
Normal file
119
packages/flux-cms/core/vendor/brianium/paratest/src/ParaTestCommand.php
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Jean85\PrettyVersions;
|
||||
use ParaTest\WrapperRunner\WrapperRunner;
|
||||
use PHPUnit\Runner\Version;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function is_string;
|
||||
use function is_subclass_of;
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
final class ParaTestCommand extends Command
|
||||
{
|
||||
public const COMMAND_NAME = 'paratest';
|
||||
|
||||
private const KNOWN_RUNNERS = [
|
||||
'WrapperRunner' => WrapperRunner::class,
|
||||
];
|
||||
|
||||
/** @param non-empty-string $cwd */
|
||||
public function __construct(
|
||||
private readonly string $cwd,
|
||||
?string $name = null
|
||||
) {
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/** @param non-empty-string $cwd */
|
||||
public static function applicationFactory(string $cwd): Application
|
||||
{
|
||||
$application = new Application();
|
||||
$command = new self($cwd, self::COMMAND_NAME);
|
||||
|
||||
$application->setName('ParaTest');
|
||||
$application->setVersion(PrettyVersions::getVersion('brianium/paratest')->getPrettyVersion());
|
||||
$application->add($command);
|
||||
$commandName = $command->getName();
|
||||
assert($commandName !== null);
|
||||
$application->setDefaultCommand($commandName, true);
|
||||
|
||||
return $application;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
Options::setInputDefinition($this->getDefinition());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function mergeApplicationDefinition($mergeArgs = true): void
|
||||
{
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$application = $this->getApplication();
|
||||
assert($application !== null);
|
||||
|
||||
$output->write(sprintf(
|
||||
"%s upon %s\n\n",
|
||||
$application->getLongVersion(),
|
||||
Version::getVersionString(),
|
||||
));
|
||||
|
||||
$options = Options::fromConsoleInput(
|
||||
$input,
|
||||
$this->cwd,
|
||||
);
|
||||
if (! $options->configuration->hasConfigurationFile() && ! $options->configuration->hasCliArguments()) {
|
||||
return $this->displayHelp($output);
|
||||
}
|
||||
|
||||
$runnerClass = $this->getRunnerClass($input);
|
||||
|
||||
return (new $runnerClass($options, $output))->run();
|
||||
}
|
||||
|
||||
private function displayHelp(OutputInterface $output): int
|
||||
{
|
||||
$app = $this->getApplication();
|
||||
assert($app !== null);
|
||||
$help = $app->find('help');
|
||||
$input = new ArrayInput(['command_name' => $this->getName()]);
|
||||
|
||||
return $help->run($input, $output);
|
||||
}
|
||||
|
||||
/** @return class-string<RunnerInterface> */
|
||||
private function getRunnerClass(InputInterface $input): string
|
||||
{
|
||||
$runnerClass = $input->getOption('runner');
|
||||
assert(is_string($runnerClass));
|
||||
$runnerClass = self::KNOWN_RUNNERS[$runnerClass] ?? $runnerClass;
|
||||
|
||||
if (! class_exists($runnerClass) || ! is_subclass_of($runnerClass, RunnerInterface::class)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Selected runner class "%s" does not exist or does not implement %s',
|
||||
$runnerClass,
|
||||
RunnerInterface::class,
|
||||
));
|
||||
}
|
||||
|
||||
return $runnerClass;
|
||||
}
|
||||
}
|
||||
14
packages/flux-cms/core/vendor/brianium/paratest/src/RunnerInterface.php
vendored
Normal file
14
packages/flux-cms/core/vendor/brianium/paratest/src/RunnerInterface.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest;
|
||||
|
||||
interface RunnerInterface
|
||||
{
|
||||
public const SUCCESS_EXIT = 0;
|
||||
public const FAILURE_EXIT = 1;
|
||||
public const EXCEPTION_EXIT = 2;
|
||||
|
||||
public function run(): int;
|
||||
}
|
||||
81
packages/flux-cms/core/vendor/brianium/paratest/src/Util/PhpstormHelper.php
vendored
Normal file
81
packages/flux-cms/core/vendor/brianium/paratest/src/Util/PhpstormHelper.php
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\Util;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function array_search;
|
||||
use function array_unshift;
|
||||
use function in_array;
|
||||
use function str_ends_with;
|
||||
|
||||
/** @internal */
|
||||
final readonly class PhpstormHelper
|
||||
{
|
||||
/** @param array<int, string> $argv */
|
||||
public static function handleArgvFromPhpstorm(array &$argv, string $paratestBinary): string
|
||||
{
|
||||
$phpunitKey = self::getArgvKeyFor($argv, '/phpunit');
|
||||
|
||||
if (! in_array('--filter', $argv, true)) {
|
||||
$coverageArgKey = self::getCoverageArgvKey($argv);
|
||||
if ($coverageArgKey !== false) {
|
||||
unset($argv[$coverageArgKey]);
|
||||
}
|
||||
|
||||
unset($argv[$phpunitKey]);
|
||||
|
||||
return $paratestBinary;
|
||||
}
|
||||
|
||||
unset($argv[self::getArgvKeyFor($argv, '/paratest_for_phpstorm')]);
|
||||
$phpunitBinary = $argv[$phpunitKey];
|
||||
foreach ($argv as $index => $value) {
|
||||
if ($value === '--configuration' || $value === '--bootstrap') {
|
||||
break;
|
||||
}
|
||||
|
||||
unset($argv[$index]);
|
||||
}
|
||||
|
||||
array_unshift($argv, $phpunitBinary);
|
||||
|
||||
return $phpunitBinary;
|
||||
}
|
||||
|
||||
/** @param array<int, string> $argv */
|
||||
private static function getArgvKeyFor(array $argv, string $searchFor): int
|
||||
{
|
||||
foreach ($argv as $key => $arg) {
|
||||
if (str_ends_with($arg, $searchFor)) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("Missing path to '$searchFor'");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $argv
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
private static function getCoverageArgvKey(array $argv)
|
||||
{
|
||||
$coverageOptions = [
|
||||
'-dpcov.enabled=1',
|
||||
'-dxdebug.mode=coverage',
|
||||
];
|
||||
|
||||
foreach ($coverageOptions as $coverageOption) {
|
||||
$key = array_search($coverageOption, $argv, true);
|
||||
if ($key !== false) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
269
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/ApplicationForWrapperWorker.php
vendored
Normal file
269
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/ApplicationForWrapperWorker.php
vendored
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\RunnerInterface;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Event\TestSuite\TestSuiteBuilder;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Logging\JUnit\JunitXmlLogger;
|
||||
use PHPUnit\Logging\TeamCity\TeamCityLogger;
|
||||
use PHPUnit\Logging\TestDox\TestResultCollector;
|
||||
use PHPUnit\Runner\Baseline\CannotLoadBaselineException;
|
||||
use PHPUnit\Runner\Baseline\Reader;
|
||||
use PHPUnit\Runner\CodeCoverage;
|
||||
use PHPUnit\Runner\DeprecationCollector\Facade as DeprecationCollector;
|
||||
use PHPUnit\Runner\ErrorHandler;
|
||||
use PHPUnit\Runner\Extension\ExtensionBootstrapper;
|
||||
use PHPUnit\Runner\Extension\Facade as ExtensionFacade;
|
||||
use PHPUnit\Runner\Extension\PharLoader;
|
||||
use PHPUnit\Runner\Filter\Factory;
|
||||
use PHPUnit\Runner\ResultCache\DefaultResultCache;
|
||||
use PHPUnit\Runner\ResultCache\ResultCacheHandler;
|
||||
use PHPUnit\Runner\TestSuiteLoader;
|
||||
use PHPUnit\Runner\TestSuiteSorter;
|
||||
use PHPUnit\TestRunner\IssueFilter;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TextUI\Configuration\Builder;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
use PHPUnit\TextUI\Configuration\PhpHandler;
|
||||
use PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter;
|
||||
use PHPUnit\TextUI\Output\Default\UnexpectedOutputPrinter;
|
||||
use PHPUnit\TextUI\Output\DefaultPrinter;
|
||||
use PHPUnit\TextUI\Output\NullPrinter;
|
||||
use PHPUnit\TextUI\Output\TestDox\ResultPrinter as TestDoxResultPrinter;
|
||||
use PHPUnit\TextUI\TestSuiteFilterProcessor;
|
||||
use PHPUnit\Util\ExcludeList;
|
||||
|
||||
use function assert;
|
||||
use function file_put_contents;
|
||||
use function is_file;
|
||||
use function mt_srand;
|
||||
use function serialize;
|
||||
use function str_ends_with;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class ApplicationForWrapperWorker
|
||||
{
|
||||
private bool $hasBeenBootstrapped = false;
|
||||
private Configuration $configuration;
|
||||
private TestResultCollector $testdoxResultCollector;
|
||||
|
||||
/** @param list<string> $argv */
|
||||
public function __construct(
|
||||
private readonly array $argv,
|
||||
private readonly string $progressFile,
|
||||
private readonly string $unexpectedOutputFile,
|
||||
private readonly string $testResultFile,
|
||||
private readonly ?string $resultCacheFile,
|
||||
private readonly ?string $teamcityFile,
|
||||
private readonly ?string $testdoxFile,
|
||||
private readonly bool $testdoxColor,
|
||||
private readonly ?int $testdoxColumns,
|
||||
) {
|
||||
}
|
||||
|
||||
public function runTest(string $testPath): int
|
||||
{
|
||||
$null = strpos($testPath, "\0");
|
||||
$filter = null;
|
||||
if ($null !== false) {
|
||||
$filter = new Factory();
|
||||
$name = substr($testPath, $null + 1);
|
||||
assert($name !== '');
|
||||
$filter->addIncludeNameFilter($name);
|
||||
|
||||
$testPath = substr($testPath, 0, $null);
|
||||
}
|
||||
|
||||
$this->bootstrap();
|
||||
|
||||
if (is_file($testPath) && str_ends_with($testPath, '.phpt')) {
|
||||
$testSuite = TestSuite::empty($testPath);
|
||||
$testSuite->addTestFile($testPath);
|
||||
} else {
|
||||
$testSuiteRefl = (new TestSuiteLoader())->load($testPath);
|
||||
$testSuite = TestSuite::fromClassReflector($testSuiteRefl);
|
||||
}
|
||||
|
||||
EventFacade::emitter()->testSuiteLoaded(
|
||||
TestSuiteBuilder::from($testSuite),
|
||||
);
|
||||
|
||||
(new TestSuiteFilterProcessor())->process($this->configuration, $testSuite);
|
||||
|
||||
if ($filter !== null) {
|
||||
$testSuite->injectFilter($filter);
|
||||
|
||||
EventFacade::emitter()->testSuiteFiltered(
|
||||
TestSuiteBuilder::from($testSuite),
|
||||
);
|
||||
}
|
||||
|
||||
EventFacade::emitter()->testRunnerExecutionStarted(
|
||||
TestSuiteBuilder::from($testSuite),
|
||||
);
|
||||
|
||||
$testSuite->run();
|
||||
|
||||
return TestResultFacade::result()->wasSuccessful()
|
||||
? RunnerInterface::SUCCESS_EXIT
|
||||
: RunnerInterface::FAILURE_EXIT;
|
||||
}
|
||||
|
||||
private function bootstrap(): void
|
||||
{
|
||||
if ($this->hasBeenBootstrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExcludeList::addDirectory(__DIR__);
|
||||
EventFacade::emitter()->applicationStarted();
|
||||
|
||||
$this->configuration = (new Builder())->build($this->argv);
|
||||
|
||||
(new PhpHandler())->handle($this->configuration->php());
|
||||
|
||||
if ($this->configuration->hasBootstrap()) {
|
||||
$bootstrapFilename = $this->configuration->bootstrap();
|
||||
include_once $bootstrapFilename;
|
||||
EventFacade::emitter()->testRunnerBootstrapFinished($bootstrapFilename);
|
||||
}
|
||||
|
||||
$extensionRequiresCodeCoverageCollection = false;
|
||||
if (! $this->configuration->noExtensions()) {
|
||||
if ($this->configuration->hasPharExtensionDirectory()) {
|
||||
(new PharLoader())->loadPharExtensionsInDirectory(
|
||||
$this->configuration->pharExtensionDirectory(),
|
||||
);
|
||||
}
|
||||
|
||||
$extensionFacade = new ExtensionFacade();
|
||||
$extensionBootstrapper = new ExtensionBootstrapper(
|
||||
$this->configuration,
|
||||
$extensionFacade,
|
||||
);
|
||||
|
||||
foreach ($this->configuration->extensionBootstrappers() as $bootstrapper) {
|
||||
$extensionBootstrapper->bootstrap(
|
||||
$bootstrapper['className'],
|
||||
$bootstrapper['parameters'],
|
||||
);
|
||||
}
|
||||
|
||||
$extensionRequiresCodeCoverageCollection = $extensionFacade->requiresCodeCoverageCollection();
|
||||
}
|
||||
|
||||
if ($this->configuration->hasLogfileJunit()) {
|
||||
new JunitXmlLogger(
|
||||
DefaultPrinter::from($this->configuration->logfileJunit()),
|
||||
EventFacade::instance(),
|
||||
);
|
||||
}
|
||||
|
||||
$printer = new ProgressPrinterOutput(
|
||||
DefaultPrinter::from($this->progressFile),
|
||||
DefaultPrinter::from($this->unexpectedOutputFile),
|
||||
);
|
||||
|
||||
new UnexpectedOutputPrinter($printer, EventFacade::instance());
|
||||
new ProgressPrinter(
|
||||
$printer,
|
||||
EventFacade::instance(),
|
||||
false,
|
||||
99999,
|
||||
$this->configuration->source(),
|
||||
);
|
||||
|
||||
if (isset($this->teamcityFile)) {
|
||||
new TeamCityLogger(
|
||||
DefaultPrinter::from($this->teamcityFile),
|
||||
EventFacade::instance(),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->testdoxFile)) {
|
||||
$this->testdoxResultCollector = new TestResultCollector(
|
||||
EventFacade::instance(),
|
||||
new IssueFilter($this->configuration->source()),
|
||||
);
|
||||
}
|
||||
|
||||
TestResultFacade::init();
|
||||
DeprecationCollector::init();
|
||||
|
||||
if (isset($this->resultCacheFile)) {
|
||||
new ResultCacheHandler(
|
||||
new DefaultResultCache($this->resultCacheFile),
|
||||
EventFacade::instance(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->configuration->source()->useBaseline()) {
|
||||
$baselineFile = $this->configuration->source()->baseline();
|
||||
$baseline = null;
|
||||
|
||||
try {
|
||||
$baseline = (new Reader())->read($baselineFile);
|
||||
} catch (CannotLoadBaselineException $e) {
|
||||
EventFacade::emitter()->testRunnerTriggeredPhpunitWarning($e->getMessage());
|
||||
}
|
||||
|
||||
if ($baseline !== null) {
|
||||
ErrorHandler::instance()->useBaseline($baseline);
|
||||
}
|
||||
}
|
||||
|
||||
EventFacade::instance()->seal();
|
||||
|
||||
CodeCoverage::instance()->init(
|
||||
$this->configuration,
|
||||
CodeCoverageFilterRegistry::instance(),
|
||||
$extensionRequiresCodeCoverageCollection,
|
||||
);
|
||||
|
||||
EventFacade::emitter()->testRunnerStarted();
|
||||
|
||||
if ($this->configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
|
||||
mt_srand($this->configuration->randomOrderSeed());
|
||||
}
|
||||
|
||||
$this->hasBeenBootstrapped = true;
|
||||
}
|
||||
|
||||
public function end(): void
|
||||
{
|
||||
if (! $this->hasBeenBootstrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventFacade::emitter()->testRunnerExecutionFinished();
|
||||
EventFacade::emitter()->testRunnerFinished();
|
||||
|
||||
CodeCoverage::instance()->generateReports(new NullPrinter(), $this->configuration);
|
||||
|
||||
$result = TestResultFacade::result();
|
||||
if (isset($this->testdoxResultCollector)) {
|
||||
assert(isset($this->testdoxFile));
|
||||
assert(isset($this->testdoxColumns));
|
||||
|
||||
(new TestDoxResultPrinter(DefaultPrinter::from($this->testdoxFile), $this->testdoxColor, $this->testdoxColumns, false))->print(
|
||||
$result,
|
||||
$this->testdoxResultCollector->testMethodsGroupedByClass(),
|
||||
);
|
||||
}
|
||||
|
||||
file_put_contents($this->testResultFile, serialize($result));
|
||||
|
||||
EventFacade::emitter()->applicationFinished(0);
|
||||
}
|
||||
}
|
||||
42
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/ProgressPrinterOutput.php
vendored
Normal file
42
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/ProgressPrinterOutput.php
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use PHPUnit\TextUI\Output\Printer;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/** @internal */
|
||||
final readonly class ProgressPrinterOutput implements Printer
|
||||
{
|
||||
public function __construct(
|
||||
private Printer $progressPrinter,
|
||||
private Printer $outputPrinter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function print(string $buffer): void
|
||||
{
|
||||
// Skip anything in \PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter::printProgress except $progress
|
||||
if (
|
||||
$buffer === "\n"
|
||||
|| preg_match('/^ +$/', $buffer) === 1
|
||||
|| preg_match('/^ \\d+ \\/ \\d+ \\(...%\\)$/', $buffer) === 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
match ($buffer) {
|
||||
'E', 'F', 'I', 'N', 'D', 'R', 'W', 'S', '.' => $this->progressPrinter->print($buffer),
|
||||
default => $this->outputPrinter->print($buffer),
|
||||
};
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->progressPrinter->flush();
|
||||
$this->outputPrinter->flush();
|
||||
}
|
||||
}
|
||||
334
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/ResultPrinter.php
vendored
Normal file
334
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/ResultPrinter.php
vendored
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\Options;
|
||||
use PHPUnit\Runner\TestSuiteSorter;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Output\Default\ResultPrinter as DefaultResultPrinter;
|
||||
use PHPUnit\TextUI\Output\Printer;
|
||||
use PHPUnit\TextUI\Output\SummaryPrinter;
|
||||
use PHPUnit\Util\Color;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Selector;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
use SebastianBergmann\Timer\ResourceUsageFormatter;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function assert;
|
||||
use function fclose;
|
||||
use function feof;
|
||||
use function floor;
|
||||
use function fopen;
|
||||
use function fread;
|
||||
use function fseek;
|
||||
use function ftell;
|
||||
use function fwrite;
|
||||
use function sprintf;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_EOL;
|
||||
use const PHP_VERSION;
|
||||
|
||||
/** @internal */
|
||||
final class ResultPrinter
|
||||
{
|
||||
public readonly Printer $printer;
|
||||
|
||||
private int $numTestsWidth = 0;
|
||||
private int $maxColumn = 0;
|
||||
private int $totalCases = 0;
|
||||
private int $column = 0;
|
||||
private int $casesProcessed = 0;
|
||||
private int $numberOfColumns;
|
||||
/** @var resource|null */
|
||||
private $teamcityLogFileHandle;
|
||||
/** @var array<non-empty-string, int> */
|
||||
private array $tailPositions;
|
||||
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
private readonly Options $options
|
||||
) {
|
||||
$this->printer = new class ($this->output) implements Printer {
|
||||
public function __construct(
|
||||
private readonly OutputInterface $output,
|
||||
) {
|
||||
}
|
||||
|
||||
public function print(string $buffer): void
|
||||
{
|
||||
$this->output->write(OutputFormatter::escape($buffer));
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$this->numberOfColumns = $this->options->configuration->columns();
|
||||
|
||||
if (! $this->options->configuration->hasLogfileTeamcity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$teamcityLogFileHandle = fopen($this->options->configuration->logfileTeamcity(), 'ab+');
|
||||
assert($teamcityLogFileHandle !== false);
|
||||
$this->teamcityLogFileHandle = $teamcityLogFileHandle;
|
||||
}
|
||||
|
||||
public function setTestCount(int $testCount): void
|
||||
{
|
||||
$this->totalCases = $testCount;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->numTestsWidth = strlen((string) $this->totalCases);
|
||||
$this->maxColumn = $this->numberOfColumns
|
||||
+ (DIRECTORY_SEPARATOR === '\\' ? -1 : 0) // fix windows blank lines
|
||||
- strlen($this->getProgress());
|
||||
|
||||
// @see \PHPUnit\TextUI\TestRunner::writeMessage()
|
||||
$output = $this->output;
|
||||
$write = static function (string $type, string $message) use ($output): void {
|
||||
$output->write(sprintf("%-15s%s\n", $type . ':', $message));
|
||||
};
|
||||
|
||||
// @see \PHPUnit\TextUI\Application::writeRuntimeInformation()
|
||||
$write('Processes', (string) $this->options->processes);
|
||||
|
||||
$runtime = 'PHP ' . PHP_VERSION;
|
||||
|
||||
if ($this->options->configuration->hasCoverageReport()) {
|
||||
$filter = new Filter();
|
||||
if ($this->options->configuration->pathCoverage()) {
|
||||
$codeCoverageDriver = (new Selector())->forLineAndPathCoverage($filter); // @codeCoverageIgnore
|
||||
} else {
|
||||
$codeCoverageDriver = (new Selector())->forLineCoverage($filter);
|
||||
}
|
||||
|
||||
$runtime .= ' with ' . $codeCoverageDriver->nameAndVersion();
|
||||
}
|
||||
|
||||
$write('Runtime', $runtime);
|
||||
|
||||
if ($this->options->configuration->hasConfigurationFile()) {
|
||||
$write('Configuration', $this->options->configuration->configurationFile());
|
||||
}
|
||||
|
||||
if ($this->options->configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
|
||||
$write('Random Seed', (string) $this->options->configuration->randomOrderSeed());
|
||||
}
|
||||
|
||||
$output->write("\n");
|
||||
}
|
||||
|
||||
public function printFeedback(
|
||||
SplFileInfo $progressFile,
|
||||
SplFileInfo $outputFile,
|
||||
SplFileInfo|null $teamcityFile
|
||||
): void {
|
||||
if ($this->options->needsTeamcity && $teamcityFile !== null) {
|
||||
$teamcityProgress = $this->tailMultiple([$teamcityFile]);
|
||||
|
||||
if ($this->teamcityLogFileHandle !== null) {
|
||||
fwrite($this->teamcityLogFileHandle, $teamcityProgress);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->options->configuration->outputIsTeamCity()) {
|
||||
assert(isset($teamcityProgress));
|
||||
$this->output->write($teamcityProgress);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->options->configuration->noProgress()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unexpectedOutput = $this->tail($outputFile);
|
||||
if ($unexpectedOutput !== '') {
|
||||
$this->output->write($unexpectedOutput);
|
||||
}
|
||||
|
||||
$feedbackItems = $this->tail($progressFile);
|
||||
if ($feedbackItems === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$actualTestCount = strlen($feedbackItems);
|
||||
for ($index = 0; $index < $actualTestCount; ++$index) {
|
||||
$this->printFeedbackItem($feedbackItems[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<SplFileInfo> $teamcityFiles
|
||||
* @param list<SplFileInfo> $testdoxFiles
|
||||
*/
|
||||
public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles): void
|
||||
{
|
||||
if ($this->options->needsTeamcity) {
|
||||
$teamcityProgress = $this->tailMultiple($teamcityFiles);
|
||||
|
||||
if ($this->teamcityLogFileHandle !== null) {
|
||||
fwrite($this->teamcityLogFileHandle, $teamcityProgress);
|
||||
$resource = $this->teamcityLogFileHandle;
|
||||
$this->teamcityLogFileHandle = null;
|
||||
fclose($resource);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->options->configuration->outputIsTeamCity()) {
|
||||
assert(isset($teamcityProgress));
|
||||
$this->output->write($teamcityProgress);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printer->print(PHP_EOL . (new ResourceUsageFormatter())->resourceUsageSinceStartOfRequest() . PHP_EOL . PHP_EOL);
|
||||
|
||||
$defaultResultPrinter = new DefaultResultPrinter(
|
||||
$this->printer,
|
||||
true,
|
||||
true,
|
||||
$this->options->configuration->displayDetailsOnPhpunitDeprecations(),
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
$this->options->configuration->displayDetailsOnIncompleteTests(),
|
||||
$this->options->configuration->displayDetailsOnSkippedTests(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerDeprecations(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerErrors(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerNotices(),
|
||||
$this->options->configuration->displayDetailsOnTestsThatTriggerWarnings(),
|
||||
false,
|
||||
);
|
||||
|
||||
if ($this->options->configuration->outputIsTestDox()) {
|
||||
$this->output->write($this->tailMultiple($testdoxFiles));
|
||||
|
||||
$defaultResultPrinter = new DefaultResultPrinter(
|
||||
$this->printer,
|
||||
true,
|
||||
true,
|
||||
$this->options->configuration->displayDetailsOnPhpunitDeprecations(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
$defaultResultPrinter->print($testResult);
|
||||
|
||||
(new SummaryPrinter(
|
||||
$this->printer,
|
||||
$this->options->configuration->colors(),
|
||||
))->print($testResult);
|
||||
}
|
||||
|
||||
private function printFeedbackItem(string $item): void
|
||||
{
|
||||
$this->printFeedbackItemColor($item);
|
||||
++$this->column;
|
||||
++$this->casesProcessed;
|
||||
if ($this->column !== $this->maxColumn && $this->casesProcessed < $this->totalCases) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->casesProcessed > 0
|
||||
&& $this->casesProcessed === $this->totalCases
|
||||
&& ($pad = $this->maxColumn - $this->column) > 0
|
||||
) {
|
||||
$this->output->write(str_repeat(' ', $pad));
|
||||
}
|
||||
|
||||
$this->output->write($this->getProgress() . "\n");
|
||||
$this->column = 0;
|
||||
}
|
||||
|
||||
private function printFeedbackItemColor(string $item): void
|
||||
{
|
||||
$buffer = match ($item) {
|
||||
'E' => $this->colorizeTextBox('fg-red, bold', $item),
|
||||
'F' => $this->colorizeTextBox('bg-red, fg-white', $item),
|
||||
'I', 'N', 'D', 'R', 'W' => $this->colorizeTextBox('fg-yellow, bold', $item),
|
||||
'S' => $this->colorizeTextBox('fg-cyan, bold', $item),
|
||||
'.' => $item,
|
||||
};
|
||||
$this->output->write($buffer);
|
||||
}
|
||||
|
||||
private function getProgress(): string
|
||||
{
|
||||
return sprintf(
|
||||
' %' . $this->numTestsWidth . 'd / %' . $this->numTestsWidth . 'd (%3s%%)',
|
||||
$this->casesProcessed,
|
||||
$this->totalCases,
|
||||
floor(($this->totalCases > 0 ? $this->casesProcessed / $this->totalCases : 0) * 100),
|
||||
);
|
||||
}
|
||||
|
||||
private function colorizeTextBox(string $color, string $buffer): string
|
||||
{
|
||||
if (! $this->options->configuration->colors()) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
return Color::colorizeTextBox($color, $buffer);
|
||||
}
|
||||
|
||||
/** @param list<SplFileInfo> $files */
|
||||
private function tailMultiple(array $files): string
|
||||
{
|
||||
$content = '';
|
||||
foreach ($files as $file) {
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content .= $this->tail($file);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function tail(SplFileInfo $file): string
|
||||
{
|
||||
$path = $file->getPathname();
|
||||
assert($path !== '');
|
||||
$handle = fopen($path, 'r');
|
||||
assert($handle !== false);
|
||||
$fseek = fseek($handle, $this->tailPositions[$path] ?? 0);
|
||||
assert($fseek === 0);
|
||||
|
||||
$contents = '';
|
||||
while (! feof($handle)) {
|
||||
$fread = fread($handle, 8192);
|
||||
assert($fread !== false);
|
||||
$contents .= $fread;
|
||||
}
|
||||
|
||||
$ftell = ftell($handle);
|
||||
assert($ftell !== false);
|
||||
$this->tailPositions[$path] = $ftell;
|
||||
fclose($handle);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
217
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/SuiteLoader.php
vendored
Normal file
217
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/SuiteLoader.php
vendored
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use Generator;
|
||||
use ParaTest\Options;
|
||||
use PHPUnit\Event\Facade as EventFacade;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Runner\Extension\ExtensionBootstrapper;
|
||||
use PHPUnit\Runner\Extension\Facade as ExtensionFacade;
|
||||
use PHPUnit\Runner\Extension\PharLoader;
|
||||
use PHPUnit\Runner\PhptTestCase;
|
||||
use PHPUnit\Runner\ResultCache\DefaultResultCache;
|
||||
use PHPUnit\Runner\ResultCache\NullResultCache;
|
||||
use PHPUnit\Runner\TestSuiteSorter;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TextUI\Command\Result;
|
||||
use PHPUnit\TextUI\Command\WarmCodeCoverageCacheCommand;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\TextUI\Configuration\PhpHandler;
|
||||
use PHPUnit\TextUI\Configuration\TestSuiteBuilder;
|
||||
use PHPUnit\TextUI\TestSuiteFilterProcessor;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function array_keys;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
use function mt_srand;
|
||||
use function ob_get_clean;
|
||||
use function ob_start;
|
||||
use function preg_quote;
|
||||
use function sprintf;
|
||||
use function str_starts_with;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
/** @internal */
|
||||
final readonly class SuiteLoader
|
||||
{
|
||||
public int $testCount;
|
||||
/** @var list<non-empty-string> */
|
||||
public array $tests;
|
||||
|
||||
public function __construct(
|
||||
private Options $options,
|
||||
OutputInterface $output,
|
||||
CodeCoverageFilterRegistry $codeCoverageFilterRegistry,
|
||||
) {
|
||||
(new PhpHandler())->handle($this->options->configuration->php());
|
||||
|
||||
if ($this->options->configuration->hasBootstrap()) {
|
||||
$bootstrapFilename = $this->options->configuration->bootstrap();
|
||||
include_once $bootstrapFilename;
|
||||
EventFacade::emitter()->testRunnerBootstrapFinished($bootstrapFilename);
|
||||
}
|
||||
|
||||
if (! $this->options->configuration->noExtensions()) {
|
||||
if ($this->options->configuration->hasPharExtensionDirectory()) {
|
||||
(new PharLoader())->loadPharExtensionsInDirectory(
|
||||
$this->options->configuration->pharExtensionDirectory(),
|
||||
);
|
||||
}
|
||||
|
||||
$extensionFacade = new ExtensionFacade();
|
||||
$extensionBootstrapper = new ExtensionBootstrapper(
|
||||
$this->options->configuration,
|
||||
$extensionFacade,
|
||||
);
|
||||
|
||||
foreach ($this->options->configuration->extensionBootstrappers() as $bootstrapper) {
|
||||
$extensionBootstrapper->bootstrap(
|
||||
$bootstrapper['className'],
|
||||
$bootstrapper['parameters'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TestResultFacade::init();
|
||||
EventFacade::instance()->seal();
|
||||
|
||||
$testSuite = (new TestSuiteBuilder())->build($this->options->configuration);
|
||||
|
||||
if ($this->options->configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) {
|
||||
mt_srand($this->options->configuration->randomOrderSeed());
|
||||
}
|
||||
|
||||
if (
|
||||
$this->options->configuration->executionOrder() !== TestSuiteSorter::ORDER_DEFAULT ||
|
||||
$this->options->configuration->executionOrderDefects() !== TestSuiteSorter::ORDER_DEFAULT ||
|
||||
$this->options->configuration->resolveDependencies()
|
||||
) {
|
||||
$resultCache = new NullResultCache();
|
||||
if ($this->options->configuration->cacheResult()) {
|
||||
$resultCache = new DefaultResultCache($this->options->configuration->testResultCacheFile());
|
||||
$resultCache->load();
|
||||
}
|
||||
|
||||
(new TestSuiteSorter($resultCache))->reorderTestsInSuite(
|
||||
$testSuite,
|
||||
$this->options->configuration->executionOrder(),
|
||||
$this->options->configuration->resolveDependencies(),
|
||||
$this->options->configuration->executionOrderDefects(),
|
||||
);
|
||||
}
|
||||
|
||||
(new TestSuiteFilterProcessor())->process($this->options->configuration, $testSuite);
|
||||
|
||||
$this->testCount = count($testSuite);
|
||||
|
||||
$files = [];
|
||||
$tests = [];
|
||||
foreach ($this->loadFiles($testSuite) as $file => $test) {
|
||||
$files[$file] = null;
|
||||
|
||||
if ($test instanceof PhptTestCase) {
|
||||
$tests[] = $file;
|
||||
} else {
|
||||
$name = $test->name();
|
||||
if ($test->providedData() !== []) {
|
||||
$dataName = $test->dataName();
|
||||
if ($this->options->functional) {
|
||||
$name = sprintf('/%s%s$/', preg_quote($name, '/'), preg_quote($test->dataSetAsString(), '/'));
|
||||
} else {
|
||||
if (is_int($dataName)) {
|
||||
$name .= '#' . $dataName;
|
||||
} else {
|
||||
$name .= '@' . $dataName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$name = sprintf('/%s$/', $name);
|
||||
}
|
||||
|
||||
$tests[] = "$file\0$name";
|
||||
}
|
||||
}
|
||||
|
||||
$this->tests = $this->options->functional
|
||||
? $tests
|
||||
: array_keys($files);
|
||||
|
||||
if (! $this->options->configuration->hasCoverageReport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$result = (new WarmCodeCoverageCacheCommand(
|
||||
$this->options->configuration,
|
||||
$codeCoverageFilterRegistry,
|
||||
))->execute();
|
||||
$ob_get_clean = ob_get_clean();
|
||||
assert($ob_get_clean !== false);
|
||||
$output->write($ob_get_clean);
|
||||
$output->write($result->output());
|
||||
if ($result->shellExitCode() !== Result::SUCCESS) {
|
||||
exit($result->shellExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Generator<non-empty-string, (PhptTestCase|TestCase)> */
|
||||
private function loadFiles(TestSuite $testSuite): Generator
|
||||
{
|
||||
foreach ($testSuite as $test) {
|
||||
if ($test instanceof TestSuite) {
|
||||
yield from $this->loadFiles($test);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($test instanceof PhptTestCase) {
|
||||
$refProperty = new ReflectionProperty(PhptTestCase::class, 'filename');
|
||||
$filename = $refProperty->getValue($test);
|
||||
assert(is_string($filename) && $filename !== '');
|
||||
$filename = $this->stripCwd($filename);
|
||||
|
||||
yield $filename => $test;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($test instanceof TestCase) {
|
||||
$refClass = new ReflectionClass($test);
|
||||
$filename = $refClass->getFileName();
|
||||
assert(is_string($filename));
|
||||
$filename = $this->stripCwd($filename);
|
||||
|
||||
yield $filename => $test;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $filename
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
private function stripCwd(string $filename): string
|
||||
{
|
||||
if (! str_starts_with($filename, $this->options->cwd)) {
|
||||
return $filename;
|
||||
}
|
||||
|
||||
$substr = substr($filename, 1 + strlen($this->options->cwd));
|
||||
assert($substr !== '');
|
||||
|
||||
return $substr;
|
||||
}
|
||||
}
|
||||
43
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/WorkerCrashedException.php
vendored
Normal file
43
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/WorkerCrashedException.php
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Throwable;
|
||||
|
||||
use function escapeshellarg;
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
final class WorkerCrashedException extends RuntimeException
|
||||
{
|
||||
public static function fromProcess(Process $process, string $test, ?Throwable $previousException = null): self
|
||||
{
|
||||
$envs = '';
|
||||
foreach ($process->getEnv() as $key => $value) {
|
||||
$envs .= sprintf('%s=%s ', $key, escapeshellarg((string) $value));
|
||||
}
|
||||
|
||||
$error = sprintf(
|
||||
'The test "%s%s" failed.' . "\n\nExit Code: %s(%s)\n\nWorking directory: %s",
|
||||
$envs,
|
||||
$test,
|
||||
(string) $process->getExitCode(),
|
||||
(string) $process->getExitCodeText(),
|
||||
(string) $process->getWorkingDirectory(),
|
||||
);
|
||||
|
||||
if (! $process->isOutputDisabled()) {
|
||||
$error .= sprintf(
|
||||
"\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s",
|
||||
$process->getOutput(),
|
||||
$process->getErrorOutput(),
|
||||
);
|
||||
}
|
||||
|
||||
return new self($error, 0, $previousException);
|
||||
}
|
||||
}
|
||||
380
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/WrapperRunner.php
vendored
Normal file
380
packages/flux-cms/core/vendor/brianium/paratest/src/WrapperRunner/WrapperRunner.php
vendored
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\WrapperRunner;
|
||||
|
||||
use ParaTest\Coverage\CoverageMerger;
|
||||
use ParaTest\JUnit\LogMerger;
|
||||
use ParaTest\JUnit\Writer;
|
||||
use ParaTest\Options;
|
||||
use ParaTest\RunnerInterface;
|
||||
use PHPUnit\Runner\CodeCoverage;
|
||||
use PHPUnit\Runner\ResultCache\DefaultResultCache;
|
||||
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||
use PHPUnit\TextUI\ShellExitCodeCalculator;
|
||||
use PHPUnit\Util\ExcludeList;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function array_merge;
|
||||
use function array_merge_recursive;
|
||||
use function array_shift;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_get_contents;
|
||||
use function max;
|
||||
use function realpath;
|
||||
use function unlink;
|
||||
use function unserialize;
|
||||
use function usleep;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/** @internal */
|
||||
final class WrapperRunner implements RunnerInterface
|
||||
{
|
||||
private const CYCLE_SLEEP = 10000;
|
||||
private readonly ResultPrinter $printer;
|
||||
|
||||
/** @var list<non-empty-string> */
|
||||
private array $pending = [];
|
||||
private int $exitcode = -1;
|
||||
/** @var array<positive-int,WrapperWorker> */
|
||||
private array $workers = [];
|
||||
/** @var array<int,int> */
|
||||
private array $batches = [];
|
||||
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $statusFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $progressFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $unexpectedOutputFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $resultCacheFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $testResultFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $coverageFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $junitFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $teamcityFiles = [];
|
||||
/** @var list<SplFileInfo> */
|
||||
private array $testdoxFiles = [];
|
||||
/** @var non-empty-string[] */
|
||||
private readonly array $parameters;
|
||||
private CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
|
||||
|
||||
public function __construct(
|
||||
private readonly Options $options,
|
||||
private readonly OutputInterface $output
|
||||
) {
|
||||
$this->printer = new ResultPrinter($output, $options);
|
||||
|
||||
$wrapper = realpath(
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'phpunit-wrapper.php',
|
||||
);
|
||||
assert($wrapper !== false);
|
||||
$phpFinder = new PhpExecutableFinder();
|
||||
$phpBin = $phpFinder->find(false);
|
||||
assert($phpBin !== false);
|
||||
$parameters = [$phpBin];
|
||||
$parameters = array_merge($parameters, $phpFinder->findArguments());
|
||||
|
||||
if ($options->passthruPhp !== null) {
|
||||
$parameters = array_merge($parameters, $options->passthruPhp);
|
||||
}
|
||||
|
||||
$parameters[] = $wrapper;
|
||||
|
||||
$this->parameters = $parameters;
|
||||
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry();
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$directory = dirname(__DIR__);
|
||||
assert($directory !== '');
|
||||
ExcludeList::addDirectory($directory);
|
||||
$suiteLoader = new SuiteLoader(
|
||||
$this->options,
|
||||
$this->output,
|
||||
$this->codeCoverageFilterRegistry,
|
||||
);
|
||||
$result = TestResultFacade::result();
|
||||
|
||||
$this->pending = $suiteLoader->tests;
|
||||
$this->printer->setTestCount($suiteLoader->testCount);
|
||||
$this->printer->start();
|
||||
$this->startWorkers();
|
||||
$this->assignAllPendingTests();
|
||||
$this->waitForAllToFinish();
|
||||
|
||||
return $this->complete($result);
|
||||
}
|
||||
|
||||
private function startWorkers(): void
|
||||
{
|
||||
for ($token = 1; $token <= $this->options->processes; ++$token) {
|
||||
$this->startWorker($token);
|
||||
}
|
||||
}
|
||||
|
||||
private function assignAllPendingTests(): void
|
||||
{
|
||||
$batchSize = $this->options->maxBatchSize;
|
||||
|
||||
while (count($this->pending) > 0 && count($this->workers) > 0) {
|
||||
foreach ($this->workers as $token => $worker) {
|
||||
if (! $worker->isRunning()) {
|
||||
throw $worker->getWorkerCrashedException();
|
||||
}
|
||||
|
||||
if (! $worker->isFree()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->flushWorker($worker);
|
||||
|
||||
if ($batchSize !== 0 && $this->batches[$token] === $batchSize) {
|
||||
$this->destroyWorker($token);
|
||||
$worker = $this->startWorker($token);
|
||||
}
|
||||
|
||||
if (
|
||||
$this->exitcode > 0
|
||||
&& $this->options->configuration->stopOnFailure()
|
||||
) {
|
||||
$this->pending = [];
|
||||
} elseif (($pending = array_shift($this->pending)) !== null) {
|
||||
$worker->assign($pending);
|
||||
$this->batches[$token]++;
|
||||
}
|
||||
}
|
||||
|
||||
usleep(self::CYCLE_SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
private function flushWorker(WrapperWorker $worker): void
|
||||
{
|
||||
$this->exitcode = max($this->exitcode, $worker->getExitCode());
|
||||
$this->printer->printFeedback(
|
||||
$worker->progressFile,
|
||||
$worker->unexpectedOutputFile,
|
||||
$worker->teamcityFile ?? null,
|
||||
);
|
||||
$worker->reset();
|
||||
}
|
||||
|
||||
private function waitForAllToFinish(): void
|
||||
{
|
||||
$stopped = [];
|
||||
while (count($this->workers) > 0) {
|
||||
foreach ($this->workers as $index => $worker) {
|
||||
if ($worker->isRunning()) {
|
||||
if (! isset($stopped[$index]) && $worker->isFree()) {
|
||||
$worker->stop();
|
||||
$stopped[$index] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $worker->isFree()) {
|
||||
throw $worker->getWorkerCrashedException();
|
||||
}
|
||||
|
||||
$this->flushWorker($worker);
|
||||
unset($this->workers[$index]);
|
||||
}
|
||||
|
||||
usleep(self::CYCLE_SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param positive-int $token */
|
||||
private function startWorker(int $token): WrapperWorker
|
||||
{
|
||||
$worker = new WrapperWorker(
|
||||
$this->output,
|
||||
$this->options,
|
||||
$this->parameters,
|
||||
$token,
|
||||
);
|
||||
$worker->start();
|
||||
$this->batches[$token] = 0;
|
||||
|
||||
$this->statusFiles[] = $worker->statusFile;
|
||||
$this->progressFiles[] = $worker->progressFile;
|
||||
$this->unexpectedOutputFiles[] = $worker->unexpectedOutputFile;
|
||||
$this->testResultFiles[] = $worker->testResultFile;
|
||||
|
||||
if (isset($worker->resultCacheFile)) {
|
||||
$this->resultCacheFiles[] = $worker->resultCacheFile;
|
||||
}
|
||||
|
||||
if (isset($worker->junitFile)) {
|
||||
$this->junitFiles[] = $worker->junitFile;
|
||||
}
|
||||
|
||||
if (isset($worker->coverageFile)) {
|
||||
$this->coverageFiles[] = $worker->coverageFile;
|
||||
}
|
||||
|
||||
if (isset($worker->teamcityFile)) {
|
||||
$this->teamcityFiles[] = $worker->teamcityFile;
|
||||
}
|
||||
|
||||
if (isset($worker->testdoxFile)) {
|
||||
$this->testdoxFiles[] = $worker->testdoxFile;
|
||||
}
|
||||
|
||||
return $this->workers[$token] = $worker;
|
||||
}
|
||||
|
||||
private function destroyWorker(int $token): void
|
||||
{
|
||||
$this->workers[$token]->stop();
|
||||
// We need to wait for ApplicationForWrapperWorker::end to end
|
||||
while ($this->workers[$token]->isRunning()) {
|
||||
usleep(self::CYCLE_SLEEP);
|
||||
}
|
||||
|
||||
unset($this->workers[$token]);
|
||||
}
|
||||
|
||||
private function complete(TestResult $testResultSum): int
|
||||
{
|
||||
foreach ($this->testResultFiles as $testresultFile) {
|
||||
if (! $testresultFile->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($testresultFile->getPathname());
|
||||
assert($contents !== false);
|
||||
$testResult = unserialize($contents);
|
||||
assert($testResult instanceof TestResult);
|
||||
|
||||
$testResultSum = new TestResult(
|
||||
(int) $testResultSum->hasTests() + (int) $testResult->hasTests(),
|
||||
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
||||
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
|
||||
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
||||
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
||||
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
||||
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
||||
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
||||
array_merge_recursive($testResultSum->errors(), $testResult->errors()),
|
||||
array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()),
|
||||
array_merge_recursive($testResultSum->notices(), $testResult->notices()),
|
||||
array_merge_recursive($testResultSum->warnings(), $testResult->warnings()),
|
||||
array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()),
|
||||
array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()),
|
||||
array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()),
|
||||
$testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->options->configuration->cacheResult()) {
|
||||
$resultCacheSum = new DefaultResultCache($this->options->configuration->testResultCacheFile());
|
||||
foreach ($this->resultCacheFiles as $resultCacheFile) {
|
||||
$resultCache = new DefaultResultCache($resultCacheFile->getPathname());
|
||||
$resultCache->load();
|
||||
|
||||
$resultCacheSum->mergeWith($resultCache);
|
||||
}
|
||||
|
||||
$resultCacheSum->persist();
|
||||
}
|
||||
|
||||
$this->printer->printResults(
|
||||
$testResultSum,
|
||||
$this->teamcityFiles,
|
||||
$this->testdoxFiles,
|
||||
);
|
||||
$this->generateCodeCoverageReports();
|
||||
$this->generateLogs();
|
||||
|
||||
$exitcode = (new ShellExitCodeCalculator())->calculate(
|
||||
$this->options->configuration,
|
||||
$testResultSum,
|
||||
);
|
||||
|
||||
$this->clearFiles($this->statusFiles);
|
||||
$this->clearFiles($this->progressFiles);
|
||||
$this->clearFiles($this->unexpectedOutputFiles);
|
||||
$this->clearFiles($this->testResultFiles);
|
||||
$this->clearFiles($this->resultCacheFiles);
|
||||
$this->clearFiles($this->coverageFiles);
|
||||
$this->clearFiles($this->junitFiles);
|
||||
$this->clearFiles($this->teamcityFiles);
|
||||
$this->clearFiles($this->testdoxFiles);
|
||||
|
||||
return $exitcode;
|
||||
}
|
||||
|
||||
protected function generateCodeCoverageReports(): void
|
||||
{
|
||||
if ($this->coverageFiles === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverageManager = new CodeCoverage();
|
||||
$coverageManager->init(
|
||||
$this->options->configuration,
|
||||
$this->codeCoverageFilterRegistry,
|
||||
false,
|
||||
);
|
||||
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
|
||||
foreach ($this->coverageFiles as $coverageFile) {
|
||||
$coverageMerger->addCoverageFromFile($coverageFile);
|
||||
}
|
||||
|
||||
$coverageManager->generateReports(
|
||||
$this->printer->printer,
|
||||
$this->options->configuration,
|
||||
);
|
||||
}
|
||||
|
||||
private function generateLogs(): void
|
||||
{
|
||||
if ($this->junitFiles === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$testSuite = (new LogMerger())->merge($this->junitFiles);
|
||||
if ($testSuite === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
(new Writer())->write(
|
||||
$testSuite,
|
||||
$this->options->configuration->logfileJunit(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<SplFileInfo> $files */
|
||||
private function clearFiles(array $files): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue