diff --git a/composer.json b/composer.json index 151b4edf9..949510f73 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ "require": { "php": "8.3.*|8.4.*", "ext-dom": "*", + "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "ext-intl": "*", "blade-ui-kit/blade-icons": "^1.6", "code16/laravel-content-renderer": "^1.1", "inertiajs/inertia-laravel": "^2.0", diff --git a/demo/app/Providers/DemoSharpServiceProvider.php b/demo/app/Providers/DemoSharpServiceProvider.php index 4a26d2535..97ab9ac44 100644 --- a/demo/app/Providers/DemoSharpServiceProvider.php +++ b/demo/app/Providers/DemoSharpServiceProvider.php @@ -7,13 +7,6 @@ use App\Sharp\AppSearchEngine; use App\Sharp\Demo2faNotificationHandler; use App\Sharp\DummyGlobalFilter; -use App\Sharp\Entities\AuthorEntity; -use App\Sharp\Entities\CategoryEntity; -use App\Sharp\Entities\DemoDashboardEntity; -use App\Sharp\Entities\PostBlockEntity; -use App\Sharp\Entities\PostEntity; -use App\Sharp\Entities\ProfileEntity; -use App\Sharp\Entities\TestEntity; use App\Sharp\SharpMenu; use Code16\Sharp\Config\SharpConfigBuilder; use Code16\Sharp\SharpAppServiceProvider; @@ -24,19 +17,12 @@ protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('Demo project') - ->addEntity('posts', PostEntity::class) - ->addEntity('blocks', PostBlockEntity::class) - ->addEntity('categories', CategoryEntity::class) - ->addEntity('authors', AuthorEntity::class) - ->addEntity('profile', ProfileEntity::class) - ->addEntity('dashboard', DemoDashboardEntity::class) - ->addEntity('test', TestEntity::class) + ->autodiscoverEntities() ->addGlobalFilter(DummyGlobalFilter::class) ->configureUploadsThumbnailCreation(uploadModelClass: Media::class) ->setSharpMenu(SharpMenu::class) ->setThemeColor('#004c9b') ->setThemeLogo(logoUrl: '/img/sharp/logo.svg', logoHeight: '1rem', faviconUrl: '/img/sharp/favicon-32x32.png') -// ->redirectLoginToUrl('/my-login') ->enableImpersonation() ->enableForgottenPassword() ->setAuthCustomGuard('web') diff --git a/demo/app/Sharp/Categories/CategoryShow.php b/demo/app/Sharp/Categories/CategoryShow.php index 7d2f0fca4..8950ecc6f 100644 --- a/demo/app/Sharp/Categories/CategoryShow.php +++ b/demo/app/Sharp/Categories/CategoryShow.php @@ -3,6 +3,7 @@ namespace App\Sharp\Categories; use App\Models\Category; +use App\Sharp\Entities\PostEntity; use App\Sharp\Utils\Filters\CategoryFilter; use Code16\Sharp\Show\Fields\SharpShowEntityListField; use Code16\Sharp\Show\Fields\SharpShowTextField; @@ -36,7 +37,7 @@ protected function buildShowFields(FieldsContainer $showFields): void ->setLabel('Description') ) ->addField( - SharpShowEntityListField::make('posts') + SharpShowEntityListField::make(PostEntity::class) ->setLabel('Related posts') ->showCreateButton(false) ->showCount() @@ -54,7 +55,7 @@ protected function buildShowLayout(ShowLayout $showLayout): void ->withField('description'); }); }) - ->addEntityListSection('posts'); + ->addEntityListSection(PostEntity::class); } public function delete($id): void diff --git a/demo/app/Sharp/Entities/DemoDashboardEntity.php b/demo/app/Sharp/Entities/DemoDashboardEntity.php index 9f11eee16..2d7a22f1f 100644 --- a/demo/app/Sharp/Entities/DemoDashboardEntity.php +++ b/demo/app/Sharp/Entities/DemoDashboardEntity.php @@ -7,5 +7,6 @@ class DemoDashboardEntity extends SharpDashboardEntity { + public static string $entityKey = 'dashboard'; protected ?string $view = DemoDashboard::class; } diff --git a/demo/app/Sharp/Posts/Blocks/AbstractPostBlockForm.php b/demo/app/Sharp/Posts/Blocks/AbstractPostBlockForm.php index 0d5c19924..1d46cb009 100644 --- a/demo/app/Sharp/Posts/Blocks/AbstractPostBlockForm.php +++ b/demo/app/Sharp/Posts/Blocks/AbstractPostBlockForm.php @@ -3,6 +3,7 @@ namespace App\Sharp\Posts\Blocks; use App\Models\PostBlock; +use App\Sharp\Entities\PostEntity; use Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater; use Code16\Sharp\Form\Fields\SharpFormField; use Code16\Sharp\Form\Fields\SharpFormHtmlField; @@ -87,7 +88,7 @@ public function update($id, array $data) ? PostBlock::findOrFail($id) : new PostBlock([ 'type' => static::$postBlockType, - 'post_id' => sharp()->context()->breadcrumb()->previousShowSegment('posts')->instanceId(), + 'post_id' => sharp()->context()->breadcrumb()->previousShowSegment(PostEntity::class)->instanceId(), ]); $this->save($postBlock, $data); diff --git a/demo/app/Sharp/Posts/Blocks/PostBlockPolicy.php b/demo/app/Sharp/Posts/Blocks/PostBlockPolicy.php index eea195c4d..9445aba72 100644 --- a/demo/app/Sharp/Posts/Blocks/PostBlockPolicy.php +++ b/demo/app/Sharp/Posts/Blocks/PostBlockPolicy.php @@ -4,6 +4,7 @@ use App\Models\Post; use App\Models\PostBlock; +use App\Sharp\Entities\PostEntity; use Code16\Sharp\Auth\SharpEntityPolicy; class PostBlockPolicy extends SharpEntityPolicy @@ -17,6 +18,6 @@ public function view($user, $instanceId): bool public function create($user): bool { return $user->isAdmin() - || Post::find(sharp()->context()->breadcrumb()->previousShowSegment('posts')->instanceId())?->author_id === $user->id; + || Post::find(sharp()->context()->breadcrumb()->previousShowSegment(PostEntity::class)->instanceId())?->author_id === $user->id; } } diff --git a/demo/app/Sharp/Posts/Blocks/PostBlockVisualsForm.php b/demo/app/Sharp/Posts/Blocks/PostBlockVisualsForm.php index 2667c09b2..3de7f31f0 100644 --- a/demo/app/Sharp/Posts/Blocks/PostBlockVisualsForm.php +++ b/demo/app/Sharp/Posts/Blocks/PostBlockVisualsForm.php @@ -2,6 +2,7 @@ namespace App\Sharp\Posts\Blocks; +use App\Sharp\Entities\PostEntity; use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer; use Code16\Sharp\Form\Fields\SharpFormField; use Code16\Sharp\Form\Fields\SharpFormListField; @@ -37,7 +38,7 @@ protected function buildAdditionalFields(FieldsContainer $formFields): void ->setStorageBasePath(function () { return sprintf( 'data/posts/%s/blocks/{id}', - sharp()->context()->breadcrumb()->previousShowSegment('posts')->instanceId(), + sharp()->context()->breadcrumb()->previousShowSegment(PostEntity::class)->instanceId(), ); }), ) diff --git a/demo/app/Sharp/Posts/PostShow.php b/demo/app/Sharp/Posts/PostShow.php index 3a612db6d..3fa282417 100644 --- a/demo/app/Sharp/Posts/PostShow.php +++ b/demo/app/Sharp/Posts/PostShow.php @@ -5,6 +5,7 @@ use App\Models\Post; use App\Sharp\Entities\AuthorEntity; use App\Sharp\Entities\CategoryEntity; +use App\Sharp\Entities\PostBlockEntity; use App\Sharp\Posts\Commands\EvaluateDraftPostWizardCommand; use App\Sharp\Posts\Commands\PreviewPostCommand; use App\Sharp\Utils\Embeds\AuthorEmbed; @@ -63,7 +64,7 @@ protected function buildShowFields(FieldsContainer $showFields): void ) ) ->addField( - SharpShowEntityListField::make('blocks') + SharpShowEntityListField::make(PostBlockEntity::class) ->setLabel('Blocks') ->hideFilterWithValue('post', fn ($instanceId) => $instanceId) ); @@ -92,7 +93,7 @@ protected function buildShowLayout(ShowLayout $showLayout): void $column->withField('content'); }); }) - ->addEntityListSection('blocks'); + ->addEntityListSection(PostBlockEntity::class); } public function buildShowConfig(): void diff --git a/demo/composer.lock b/demo/composer.lock index c8955d43b..fd8ca921a 100644 --- a/demo/composer.lock +++ b/demo/composer.lock @@ -4,8 +4,816 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dd087c87d1557269d23566a4007c223a", + "content-hash": "bb781846247e4dba01c63210253ffbd6", "packages": [ + { + "name": "amphp/amp", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-26T16:07:39+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/daa00f2efdbd71565bf64ffefa89e37542addf93", + "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-02-17T04:49:38+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "5113111de02796a782f5d90767455e7391cca190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", + "reference": "5113111de02796a782f5d90767455e7391cca190", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-12-21T01:56:09+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "97cbf289f4d8877acfe58dd90ed5a4370a43caa4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/97cbf289f4d8877acfe58dd90ed5a4370a43caa4", + "reference": "97cbf289f4d8877acfe58dd90ed5a4370a43caa4", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:42:46+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, { "name": "bacon/bacon-qr-code", "version": "2.0.8", @@ -384,6 +1192,50 @@ }, "time": "2024-08-09T14:30:48+00:00" }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -1598,6 +2450,64 @@ ], "time": "2024-06-15T08:20:20+00:00" }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, { "name": "laravel/framework", "version": "v11.35.0", @@ -4089,6 +4999,78 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, { "name": "spatie/image-optimizer", "version": "1.8.0", @@ -4287,6 +5269,85 @@ ], "time": "2024-12-11T09:51:14+00:00" }, + { + "name": "spatie/php-structure-discoverer", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/php-structure-discoverer.git", + "reference": "42f4d731d3dd4b3b85732e05a8c1928fcfa2f4bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/42f4d731d3dd4b3b85732e05a8c1928fcfa2f4bc", + "reference": "42f4d731d3dd4b3b85732e05a8c1928fcfa2f4bc", + "shasum": "" + }, + "require": { + "amphp/amp": "^v3.0", + "amphp/parallel": "^2.2", + "illuminate/collections": "^10.0|^11.0|^12.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.4.3", + "symfony/finder": "^6.0|^7.0" + }, + "require-dev": { + "illuminate/console": "^10.0|^11.0|^12.0", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5|^10.0|^11.5.3", + "spatie/laravel-ray": "^1.26" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\StructureDiscoverer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Automatically discover structures within your PHP application", + "homepage": "https://github.com/spatie/php-structure-discoverer", + "keywords": [ + "discover", + "laravel", + "php", + "php-structure-discoverer" + ], + "support": { + "issues": "https://github.com/spatie/php-structure-discoverer/issues", + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/LaravelAutoDiscoverer", + "type": "github" + } + ], + "time": "2025-02-14T10:18:38+00:00" + }, { "name": "symfony/clock", "version": "v7.2.0", diff --git a/docs/guide/building-dashboard.md b/docs/guide/building-dashboard.md index e92eb75d3..9bd6cdbd0 100644 --- a/docs/guide/building-dashboard.md +++ b/docs/guide/building-dashboard.md @@ -103,8 +103,8 @@ class SharpServiceProvider extends SharpAppServiceProvider protected function configureSharp(SharpConfigBuilder $config): void { $config - ->addEntity('company_dashboard', CompanyDashboardEntity::class) - // [...] + ->declareEntity(CompanyDashboardEntity::class); + // ... } } ``` @@ -117,7 +117,7 @@ class AppSharpMenu extends SharpMenu public function build(): self { return $this - ->addEntityLink('company_dashboard', 'Dashboard') + ->addEntityLink(CompanyDashboardEntity::class, 'Dashboard'); // ... } } diff --git a/docs/guide/context.md b/docs/guide/context.md index 12de79a33..fa5af0b91 100644 --- a/docs/guide/context.md +++ b/docs/guide/context.md @@ -67,11 +67,15 @@ sharp()->context()->breadcrumb(); Get the current or previous breadcrumb item. -### `previousShowSegment(?string $entityKey = null): ?BreadcrumbItem` -### `previousListSegment(?string $entityKey = null): ?BreadcrumbItem` +### `previousShowSegment(?string $entityKeyOrClassName = null): ?BreadcrumbItem` +### `previousListSegment(?string $entityKeyOrClassName = null): ?BreadcrumbItem` Get (if existing) the closest Show or List in the breadcrumb. +::: tip +As always, prefer the entity class name to the entity key. For instance: `sharp()->context()->breadcrumb()->previousShowSegment(MyEntity::class)`. +::: + ### The `BreadcrumbItem` class A `BreadcrumbItem` instance has the same methods seen above: @@ -96,7 +100,10 @@ class CommentForm extends SharpForm $comment = $id ? Comment::find($id) : new Comment([ - 'post_id' => sharp()->context()->breadcrumb()->previousShowSegment('post')->instanceId() + 'post_id' => sharp()->context() + ->breadcrumb() + ->previousShowSegment(PostEntity::class) + ->instanceId() ]); $this->save($comment, $data); diff --git a/docs/guide/entity-class.md b/docs/guide/entity-class.md index 36bddb821..36b958b68 100644 --- a/docs/guide/entity-class.md +++ b/docs/guide/entity-class.md @@ -15,7 +15,7 @@ php artisan sharp:make:entity [--label,--dashboard,--show,--form,-- ``` ::: tip -The Entity name should be singular, in CamelCase and must end with the "Entity" suffix. For instance: `ProductEntity`. +The Entity name should be singular, in CamelCase and end with the "Entity" suffix. For instance: `ProductEntity`. ::: ## Write the class @@ -84,7 +84,7 @@ class MyEntity extends SharpEntity ### Single shows and forms -When you need to configure a "unique" resource that does not fit into a List / Show schema, like for instance an account, or a configuration item, you can use a Single Show or Form. This is a dedicated topic, [documented here](single-show.md). +When you need to configure a "unique" resource that does not fit into a List / Show schema, like for instance an account or a configuration item, you can use a Single Show or Form. This is a dedicated topic, [documented here](single-show.md). ### Handle Multiforms @@ -92,7 +92,48 @@ Multiforms allows to declare different forms for the same entity, to hanle varia ## Declare the Entity in Sharp configuration -The last step is to declare the entity in Sharp, in the ServiceProvider: +The last step is to declare the entity in Sharp, in your `SharpAppServiceProvider` implementation. + +### Autodiscovery + +The easiest way is to let Sharp autodiscover your entities: + +```php +class SharpServiceProvider extends SharpAppServiceProvider +{ + protected function configureSharp(SharpConfigBuilder $config): void + { + $config + ->setName('My new project') + ->autodiscoverEntities('Sharp/Entities'); + // ... + } +} +``` + +The `autodiscoverEntities()` method will scan the given directory (path relative to `app_path()`) for all Entity classes, and declare them in Sharp. There are a few catches though: +- The entity key will be the class name, minus the Entity suffix, in kebab-case. For instance, `BestProductEntity` will be declared as `best-product` (more on this below). +- Each php file must correspond to one class, named after the file name (this should always be the case if you follow PSR-4). + +::: note +A note on entity keys: Sharp is using the entity key everywhere internally (starting in the URL). The only rule here is to have a unique key for each entity, which should always be the case with the autodiscovery mechanism (apart in tricky cases like one `ProductEntity` and another `ProductsEntity`: if you really need to do that, check the next chapter "choosing your own entity key"). +::: + +### Choosing your own entity key + +If for whatever reason you want to choose your own entity key, you can set it in the entity class: + +```php +class ProductEntity extends SharpEntity +{ + public static string $entityKey = 'my-product'; + // ... +} +``` + +### Manual declaration + +If you want to have control over the entity declaration, you can declare them manually instead of using autodiscovery: ```php class SharpServiceProvider extends SharpAppServiceProvider @@ -101,15 +142,15 @@ class SharpServiceProvider extends SharpAppServiceProvider { $config ->setName('My new project') - ->addEntity('product', ProductEntity::class); + ->declareEntity(ProductEntity::class); // ... } } ``` -## Custom Entity Resolver +### Custom Entity Resolver -In some very specific cases, you may want to have more control over the entity declaration, depending on some context. You can use a custom SharpEntityResolver to do that. +In some very specific cases, you may want to have full control over the entity declaration, depending on some context. You can use a custom `SharpEntityResolver` to do that. ```php use Code16\Sharp\Utils\Entities\SharpEntityResolver; @@ -145,5 +186,9 @@ class SharpServiceProvider extends SharpAppServiceProvider ``` ::: warning -You must remove all `->addEntity()` calls in order to use `->declareEntityResolver()`. -::: \ No newline at end of file +You must remove all `->declareEntity()` calls in order to use `->declareEntityResolver()`. +::: + +::: warning +If you are using a custom entity resolver, you won’t be able to use the `SharpEntity` classes in the [menu](building-menu.md), or in [`LinkTo` links](link-to.md), or for [embedded entity lists](show-fields/embedded-entity-list.md): you will have to use the entity key instead. For instance: `LinkToForm::make('products', $id)`. +::: diff --git a/docs/guide/index.md b/docs/guide/index.md index eebaead21..a63865cdd 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -41,7 +41,7 @@ This is a simple example to illustrate the main concepts of Sharp: we'll see in ## Installation -Sharp 9 needs Laravel 11+ and PHP 8.2+. +Sharp 9 needs Laravel 11+ and PHP 8.3+. - Add the package with composer: `composer require code16/sharp` - Then run: `php artisan sharp:install` @@ -64,12 +64,16 @@ class SharpServiceProvider extends SharpAppServiceProvider $config ->setName('My new project') ->setSharpMenu(SharpMenu::class) - ->addEntity('product', ProductEntity::class); + ->declareEntity(ProductEntity::class); // ... } } ``` +::: tip +As shown in the [Entity class](entity-class.md) documentation, you can also let Sharp autodiscover your entities. +::: + This `ProductEntity` class could be written like this: ```php @@ -109,7 +113,6 @@ class SharpServiceProvider extends SharpAppServiceProvider { $config ->setCustomUrlSegment('admin') - ->addEntity('product', ProductEntity::class) // ... } } diff --git a/docs/guide/upgrading/9.0.md b/docs/guide/upgrading/9.0.md index 3f45279fe..a1c76ae8d 100644 --- a/docs/guide/upgrading/9.0.md +++ b/docs/guide/upgrading/9.0.md @@ -53,7 +53,7 @@ class MySharpServiceProvider extends SharpAppServiceProvider { $config ->setName('My project') - ->addEntity('posts', PostEntity::class) + ->declareEntity(PostEntity::class) // ... } } @@ -98,7 +98,7 @@ class MySharpServiceProvider extends SharpAppServiceProvider ->setName('Demo project') ->setCustomUrlSegment('sharp') ->setDisplayBreadcrumb() - ->addEntity('posts', PostEntity::class) + ->declareEntity(PostEntity::class) ->addGlobalFilter(DummyGlobalFilter::class) // The auth()->id() === 1 no longer can be handled here, as the auth context is yet not available. Use the new authorize() method of the global filter instead. ->enableGlobalSearch(AppSearchEngine::class, 'Search for posts or authors...') ->setMenu(SharpMenu::class) diff --git a/src/Config/SharpConfigBuilder.php b/src/Config/SharpConfigBuilder.php index 8bc7b07b2..de05fe3fc 100644 --- a/src/Config/SharpConfigBuilder.php +++ b/src/Config/SharpConfigBuilder.php @@ -6,15 +6,24 @@ use Code16\Sharp\Auth\Impersonate\SharpDefaultEloquentImpersonationHandler; use Code16\Sharp\Auth\Impersonate\SharpImpersonationHandler; use Code16\Sharp\Auth\TwoFactor\Sharp2faHandler; +use Code16\Sharp\Exceptions\SharpInvalidConfigException; use Code16\Sharp\Exceptions\SharpInvalidEntityKeyException; use Code16\Sharp\Search\SharpSearchEngine; +use Code16\Sharp\Utils\Entities\BaseSharpEntity; +use Code16\Sharp\Utils\Entities\SharpDashboardEntity; +use Code16\Sharp\Utils\Entities\SharpEntity; use Code16\Sharp\Utils\Entities\SharpEntityResolver; use Code16\Sharp\Utils\Filters\GlobalRequiredFilter; use Code16\Sharp\Utils\Menu\SharpMenu; use Illuminate\Contracts\Auth\PasswordBroker; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Vite; +use Illuminate\Support\Str; use Illuminate\Support\Traits\Conditionable; +use ReflectionClass; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Throwable; class SharpConfigBuilder { @@ -122,6 +131,7 @@ public function displayBreadcrumb(bool $displayBreadcrumb = true): self return $this; } + /** @deprecated use declareEntity instead, and set the entityKey in the SharpEntity class */ public function addEntity(string $key, string $entityClass): self { $this->config['entities'][$key] = $entityClass; @@ -130,6 +140,31 @@ public function addEntity(string $key, string $entityClass): self return $this; } + public function declareEntity(string $entityClass): self + { + if (! is_subclass_of($entityClass, BaseSharpEntity::class)) { + throw new SharpInvalidEntityKeyException( + sprintf( + '%s is an invalid entity class: it should extend either %s or %s.', + $entityClass, SharpEntity::class, SharpDashboardEntity::class + ) + ); + } + + $entityKey = $entityClass::$entityKey ?? null; + if (! $entityKey) { + $entityKey = str(class_basename($entityClass)) + ->beforeLast('Entity') + ->kebab() + ->toString(); + } + + $this->config['entities'][$entityKey] = $entityClass; + $this->config['entity_resolver'] = null; + + return $this; + } + public function declareEntityResolver(SharpEntityResolver|string $resolver): self { $resolver = instanciate($resolver); @@ -144,6 +179,30 @@ public function declareEntityResolver(SharpEntityResolver|string $resolver): sel return $this; } + public function autodiscoverEntities(string $path = 'Sharp/Entities'): self + { + $entityClasses = collect((new Finder())->files()->in(app_path($path))) + ->map(fn (SplFileInfo $file) => $this->fullQualifiedClassNameFromFile($file)) + ->filter(function (string $entityClass) { + try { + return ( + is_subclass_of($entityClass, SharpEntity::class) + || is_subclass_of($entityClass, SharpDashboardEntity::class) + ) && (new ReflectionClass($entityClass))->isInstantiable(); + } catch (Throwable) { + return false; + } + }) + ->flatten() + ->each(fn (string $entityClass) => $this->declareEntity($entityClass)); + + if (empty($entityClasses)) { + throw new SharpInvalidConfigException('Autodiscover failed: no entities found in the given path.'); + } + + return $this; + } + public function addGlobalFilter(string|GlobalRequiredFilter $filter): self { $this->config['global_filters'][] = instanciate($filter); @@ -447,4 +506,15 @@ public function get(string $key): mixed return $this->config[$key] ?? null; } + + private function fullQualifiedClassNameFromFile(SplFileInfo $file): string + { + $class = trim(Str::replaceFirst(base_path(), '', $file->getRealPath()), DIRECTORY_SEPARATOR); + + return str_replace( + [DIRECTORY_SEPARATOR, 'App\\'], + ['\\', app()->getNamespace()], + ucfirst(Str::replaceLast('.php', '', $class)) + ); + } } diff --git a/src/Console/stubs/provider.stub b/src/Console/stubs/provider.stub index 2794014dc..6d0217b51 100644 --- a/src/Console/stubs/provider.stub +++ b/src/Console/stubs/provider.stub @@ -10,8 +10,8 @@ class DummyClass extends SharpAppServiceProvider protected function configureSharp(SharpConfigBuilder $config): void { $config - ->setName('My new project'); -// ->addEntity('posts', PostEntity::class) + ->setName('My new project') + ->autodiscoverEntities(); } protected function declareAccessGate(): void diff --git a/src/EntityList/Commands/QuickCreate/QuickCreationCommand.php b/src/EntityList/Commands/QuickCreate/QuickCreationCommand.php index 1343761af..a27c9f993 100644 --- a/src/EntityList/Commands/QuickCreate/QuickCreationCommand.php +++ b/src/EntityList/Commands/QuickCreate/QuickCreationCommand.php @@ -82,12 +82,7 @@ public function execute(array $data = []): array $currentUrl = sharp()->context()->breadcrumb()->getCurrentSegmentUrl(); return $this->sharpForm->isDisplayShowPageAfterCreation() - ? $this->link(sprintf( - '%s/s-show/%s/%s', - $currentUrl, - $this->entityKey, - $this->instanceId - )) + ? $this->link(sprintf('%s/s-show/%s/%s', $currentUrl, $this->entityKey, $this->instanceId)) : $this->reload(); } diff --git a/src/Http/Context/SharpBreadcrumb.php b/src/Http/Context/SharpBreadcrumb.php index f0dfd7602..b4502059d 100644 --- a/src/Http/Context/SharpBreadcrumb.php +++ b/src/Http/Context/SharpBreadcrumb.php @@ -62,14 +62,14 @@ public function previousSegment(): ?BreadcrumbItem return $this->breadcrumbItems()->reverse()->skip(1)->first(); } - public function previousShowSegment(?string $entityKey = null): ?BreadcrumbItem + public function previousShowSegment(?string $entityKeyOrClassName = null): ?BreadcrumbItem { - return $this->findPreviousSegment('s-show', $entityKey); + return $this->findPreviousSegment('s-show', $entityKeyOrClassName); } - public function previousListSegment(?string $entityKey = null): ?BreadcrumbItem + public function previousListSegment(?string $entityKeyOrClassName = null): ?BreadcrumbItem { - return $this->findPreviousSegment('s-list', $entityKey); + return $this->findPreviousSegment('s-list', $entityKeyOrClassName); } public function getCurrentSegmentUrl(): string @@ -111,23 +111,24 @@ public function breadcrumbItems(): Collection return $this->breadcrumbItems; } - private function findPreviousSegment(string $type, ?string $entityKey = null): ?BreadcrumbItem + private function findPreviousSegment(string $type, ?string $entityKeyOrClassName = null): ?BreadcrumbItem { $modeNotEquals = false; - if ($entityKey && Str::startsWith($entityKey, '!')) { - $entityKey = Str::substr($entityKey, 1); + if ($entityKeyOrClassName && Str::startsWith($entityKeyOrClassName, '!')) { + $entityKeyOrClassName = Str::substr($entityKeyOrClassName, 1); $modeNotEquals = true; } return $this->breadcrumbItems() ->reverse() ->filter(fn (BreadcrumbItem $item) => $item->type === $type) - ->when($entityKey !== null, fn ($items) => $items - ->filter(function (BreadcrumbItem $breadcrumbItem) use ($entityKey, $modeNotEquals) { - return $modeNotEquals - ? $breadcrumbItem->entityKey() !== $entityKey - : $breadcrumbItem->entityKey() === $entityKey; - }) + ->when($entityKeyOrClassName !== null, fn ($items) => $items + ->filter(fn (BreadcrumbItem $breadcrumbItem) => $modeNotEquals + ? $breadcrumbItem->entityKey() !== app(SharpEntityManager::class) + ->entityKeyFor($entityKeyOrClassName) + : $breadcrumbItem->entityKey() === app(SharpEntityManager::class) + ->entityKeyFor($entityKeyOrClassName) + ) ) ->first(); } diff --git a/src/Utils/Entities/BaseSharpEntity.php b/src/Utils/Entities/BaseSharpEntity.php index f9b6e5d76..c01a72476 100644 --- a/src/Utils/Entities/BaseSharpEntity.php +++ b/src/Utils/Entities/BaseSharpEntity.php @@ -8,17 +8,10 @@ abstract class BaseSharpEntity { protected bool $isDashboard = false; - protected string $entityKey = 'entity'; + public static string $entityKey; protected ?string $policy = null; protected string $label = 'entity'; - final public function setEntityKey(string $entityKey): self - { - $this->entityKey = $entityKey; - - return $this; - } - final public function getPolicyOrDefault(): SharpEntityPolicy { if (! $policy = $this->getPolicy()) { diff --git a/src/Utils/Entities/SharpDashboardEntity.php b/src/Utils/Entities/SharpDashboardEntity.php index f91d5757c..8a44e92fb 100644 --- a/src/Utils/Entities/SharpDashboardEntity.php +++ b/src/Utils/Entities/SharpDashboardEntity.php @@ -13,10 +13,12 @@ abstract class SharpDashboardEntity extends BaseSharpEntity final public function getViewOrFail(): SharpDashboard { if (! $view = $this->getView()) { - throw new SharpInvalidEntityKeyException("The view for the dashboard entity [{$this->entityKey}] was not found."); + throw new SharpInvalidEntityKeyException( + sprintf('The view for the dashboard entity %s was not found.', get_class($this)) + ); } - return $view instanceof SharpDashboard ? $view : app($view); + return $view; } final public function hasView(): bool diff --git a/src/Utils/Entities/SharpEntity.php b/src/Utils/Entities/SharpEntity.php index d9a23a183..2cf13aca1 100644 --- a/src/Utils/Entities/SharpEntity.php +++ b/src/Utils/Entities/SharpEntity.php @@ -18,7 +18,9 @@ abstract class SharpEntity extends BaseSharpEntity final public function getListOrFail(): SharpEntityList { if (! $list = $this->getList()) { - throw new SharpInvalidEntityKeyException("The list for the entity [{$this->entityKey}] was not found."); + throw new SharpInvalidEntityKeyException( + sprintf('The list for the entity [%s] was not found.', get_class($this)) + ); } return $list instanceof SharpEntityList ? $list : app($list); @@ -27,10 +29,12 @@ final public function getListOrFail(): SharpEntityList final public function getShowOrFail(): SharpShow { if (! $show = $this->getShow()) { - throw new SharpInvalidEntityKeyException("The show for the entity [{$this->entityKey}] was not found."); + throw new SharpInvalidEntityKeyException( + sprintf('The show for the entity [%s] was not found.', get_class($this)) + ); } - return $show instanceof SharpShow ? $show : app($show); + return instanciate($show); } final public function hasShow(): bool @@ -42,13 +46,17 @@ final public function getFormOrFail(?string $subEntity = null): SharpForm { if ($subEntity) { if (! $form = ($this->getMultiforms()[$subEntity][0] ?? null)) { - throw new SharpInvalidEntityKeyException("The subform for the entity [{$this->entityKey}:{$subEntity}] was not found."); + throw new SharpInvalidEntityKeyException( + sprintf('The subform for the entity [%s:%s] was not found.', get_class($this), $subEntity) + ); } } elseif (! $form = $this->getForm()) { - throw new SharpInvalidEntityKeyException("The form for the entity [{$this->entityKey}] was not found."); + throw new SharpInvalidEntityKeyException( + sprintf('The form for the entity [%s] was not found.', get_class($this)) + ); } - return $form instanceof SharpForm ? $form : app($form); + return instanciate($form); } final public function getLabelOrFail(?string $subEntity = null): string @@ -58,7 +66,9 @@ final public function getLabelOrFail(?string $subEntity = null): string : $this->getLabel(); if ($label === null) { - throw new SharpInvalidEntityKeyException("The label of the subform for the entity [{$this->entityKey}:{$subEntity}] was not found."); + throw new SharpInvalidEntityKeyException( + sprintf('The label of the subform for the entity [%s:%s] was not found.', get_class($this), $subEntity) + ); } return $label; @@ -82,7 +92,9 @@ protected function getLabel(): string protected function getList(): ?SharpEntityList { if ($this->isSingle) { - throw new SharpInvalidEntityKeyException("The entity [{$this->entityKey}] is single, and does not have a list."); + throw new SharpInvalidEntityKeyException( + sprintf('The entity [%s] is single, and does not have a list.', get_class($this)) + ); } return $this->list ? app($this->list) : null; diff --git a/src/Utils/Entities/SharpEntityManager.php b/src/Utils/Entities/SharpEntityManager.php index 6efc0b5bc..78de98f2d 100644 --- a/src/Utils/Entities/SharpEntityManager.php +++ b/src/Utils/Entities/SharpEntityManager.php @@ -13,10 +13,10 @@ public function entityFor(string $entityKey): SharpEntity|SharpDashboardEntity $entityKey = Str::before($entityKey, ':'); if (count(sharp()->config()->get('entities')) > 0) { - $entity = sharp()->config()->get('entities.'.$entityKey); - if (! $entity) { + $entityClass = sharp()->config()->get('entities.'.$entityKey); + if (! $entityClass) { // Legacy dashboard configuration (to be removed in 10.x) - $entity = sharp()->config()->get('dashboards.'.$entityKey); + $entityClass = sharp()->config()->get('dashboards.'.$entityKey); } } elseif ($sharpEntityResolver = sharp()->config()->get('entity_resolver')) { // A custom SharpEntityResolver is used @@ -24,29 +24,35 @@ public function entityFor(string $entityKey): SharpEntity|SharpDashboardEntity app()->singleton(get_class($sharpEntityResolver), fn () => $sharpEntityResolver); } - $entity = $sharpEntityResolver->entityClassName($entityKey); + $entityClass = $sharpEntityResolver->entityClassName($entityKey); } - if (isset($entity)) { - if (! app()->bound($entity)) { - app()->singleton($entity, fn () => (new $entity())->setEntityKey($entityKey)); + if (isset($entityClass)) { + if (! app()->bound($entityClass)) { + // Optimization: resolve each entity only once per request + app()->singleton($entityClass); } - return app($entity); + return app($entityClass); } - throw new SharpInvalidEntityKeyException("The entity [{$entityKey}] was not found."); + throw new SharpInvalidEntityKeyException("No entity with entity key [{$entityKey}] was found."); } public function entityKeyFor(string|BaseSharpEntity $entity): string { + if (is_string($entity) && ! class_exists($entity)) { + // Should already be an entity key + return $entity; + } + $entityClassName = is_string($entity) ? $entity : get_class($entity); $entities = sharp()->config()->get('entities'); if (! is_array($entities) || ($entityKey = array_search($entityClassName, $entities)) === false) { throw new SharpInvalidConfigException( sprintf( - 'Can’t find entityKey for [%s] (warning: this can’t work with an Entity Resolver).', + 'Can’t find entity key for [%s] (warning: this can’t work with an Entity Resolver).', $entityClassName ) ); diff --git a/src/Utils/Links/BreadcrumbBuilder.php b/src/Utils/Links/BreadcrumbBuilder.php index c64cab305..9609c0a90 100644 --- a/src/Utils/Links/BreadcrumbBuilder.php +++ b/src/Utils/Links/BreadcrumbBuilder.php @@ -58,8 +58,6 @@ public function generateUri(): string private function resolveEntityKey(string $entityClassNameOrKey): string { - return class_exists($entityClassNameOrKey) - ? app(SharpEntityManager::class)->entityKeyFor($entityClassNameOrKey) - : $entityClassNameOrKey; + return app(SharpEntityManager::class)->entityKeyFor($entityClassNameOrKey); } } diff --git a/src/Utils/Links/SharpLinkTo.php b/src/Utils/Links/SharpLinkTo.php index 790522490..5336f81a3 100644 --- a/src/Utils/Links/SharpLinkTo.php +++ b/src/Utils/Links/SharpLinkTo.php @@ -11,9 +11,7 @@ abstract class SharpLinkTo protected function __construct(string $entityClassOrKey) { - $this->entityKey = class_exists($entityClassOrKey) - ? app(SharpEntityManager::class)->entityKeyFor($entityClassOrKey) - : $entityClassOrKey; + $this->entityKey = app(SharpEntityManager::class)->entityKeyFor($entityClassOrKey); } public function setTooltip($tooltip): self diff --git a/tests-e2e/site/app/Providers/SharpServiceProvider.php b/tests-e2e/site/app/Providers/SharpServiceProvider.php index 0bce1c839..aabb17fd7 100644 --- a/tests-e2e/site/app/Providers/SharpServiceProvider.php +++ b/tests-e2e/site/app/Providers/SharpServiceProvider.php @@ -17,8 +17,8 @@ protected function configureSharp(SharpConfigBuilder $config): void $config ->setName('E2E') ->setSharpMenu(SharpMenu::class) - ->addEntity('test-models', TestModelEntity::class) - ->addEntity('test-models-single', TestModelSingleEntity::class) + ->declareEntity(TestModelEntity::class) + ->declareEntity(TestModelSingleEntity::class) ->enableImpersonation(new class() extends SharpImpersonationHandler { public function enabled(): bool diff --git a/tests-e2e/site/app/Sharp/Entities/TestModelEntity.php b/tests-e2e/site/app/Sharp/Entities/TestModelEntity.php index e422f3229..5bff7baed 100644 --- a/tests-e2e/site/app/Sharp/Entities/TestModelEntity.php +++ b/tests-e2e/site/app/Sharp/Entities/TestModelEntity.php @@ -12,6 +12,7 @@ class TestModelEntity extends SharpEntity { + public static string $entityKey = 'test-models'; protected string $label = 'Test model'; protected ?string $list = TestModelList::class; protected ?string $show = TestModelShow::class; diff --git a/tests-e2e/site/app/Sharp/Entities/TestModelSingleEntity.php b/tests-e2e/site/app/Sharp/Entities/TestModelSingleEntity.php index d1864712b..f6187f531 100644 --- a/tests-e2e/site/app/Sharp/Entities/TestModelSingleEntity.php +++ b/tests-e2e/site/app/Sharp/Entities/TestModelSingleEntity.php @@ -8,6 +8,7 @@ class TestModelSingleEntity extends SharpEntity { + public static string $entityKey = 'test-models-single'; protected string $label = 'Test model single'; protected bool $isSingle = true; protected ?string $show = TestModelSingleShow::class; diff --git a/tests/Fixtures/Entities/DashboardEntity.php b/tests/Fixtures/Entities/DashboardEntity.php index 1fdadf1af..18e38a5ab 100644 --- a/tests/Fixtures/Entities/DashboardEntity.php +++ b/tests/Fixtures/Entities/DashboardEntity.php @@ -9,6 +9,7 @@ class DashboardEntity extends SharpDashboardEntity { + public static string $entityKey = 'dashboard'; protected ?string $view = TestDashboard::class; protected ?SharpDashboard $fakeView = null; protected ?SharpEntityPolicy $fakePolicy = null; diff --git a/tests/Fixtures/Entities/PersonEntity.php b/tests/Fixtures/Entities/PersonEntity.php index 8b916a28b..df6c972d4 100644 --- a/tests/Fixtures/Entities/PersonEntity.php +++ b/tests/Fixtures/Entities/PersonEntity.php @@ -13,10 +13,10 @@ class PersonEntity extends SharpEntity { + public static string $entityKey = 'person'; public ?string $validatorForTest = null; public array $multiformValidatorsForTest = []; public ?array $fakeMultiforms = null; - protected string $entityKey = 'person'; protected string $label = 'person'; protected ?string $list = PersonList::class; protected ?SharpEntityList $fakeList; diff --git a/tests/Fixtures/Entities/SinglePersonEntity.php b/tests/Fixtures/Entities/SinglePersonEntity.php index 8b9bf52d8..e910e1c83 100644 --- a/tests/Fixtures/Entities/SinglePersonEntity.php +++ b/tests/Fixtures/Entities/SinglePersonEntity.php @@ -6,6 +6,7 @@ class SinglePersonEntity extends PersonEntity { + public static string $entityKey = 'single-person'; protected bool $isSingle = true; protected ?string $form = PersonSingleForm::class; } diff --git a/tests/Http/Api/ApiEntityListControllerTest.php b/tests/Http/Api/ApiEntityListControllerTest.php index 7cc96dd2e..c08d356c3 100644 --- a/tests/Http/Api/ApiEntityListControllerTest.php +++ b/tests/Http/Api/ApiEntityListControllerTest.php @@ -9,7 +9,7 @@ use Illuminate\Pagination\LengthAwarePaginator; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/ApiFormAutocompleteControllerTest.php b/tests/Http/Api/ApiFormAutocompleteControllerTest.php index 7cb8b9a8b..3d2ebb475 100644 --- a/tests/Http/Api/ApiFormAutocompleteControllerTest.php +++ b/tests/Http/Api/ApiFormAutocompleteControllerTest.php @@ -15,7 +15,7 @@ use Code16\Sharp\Utils\Fields\FieldsContainer; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/ApiFormUploadThumbnailControllerTest.php b/tests/Http/Api/ApiFormUploadThumbnailControllerTest.php index a5ac9a28c..4745a32b4 100644 --- a/tests/Http/Api/ApiFormUploadThumbnailControllerTest.php +++ b/tests/Http/Api/ApiFormUploadThumbnailControllerTest.php @@ -4,7 +4,7 @@ use Illuminate\Http\UploadedFile; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php index 6c2bd61a4..e3aa8d3db 100644 --- a/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListEntityCommandControllerTest.php @@ -11,7 +11,7 @@ use Illuminate\Http\UploadedFile; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiEntityListEntityStateControllerTest.php b/tests/Http/Api/Commands/ApiEntityListEntityStateControllerTest.php index e7bf1c837..74ce08a2d 100644 --- a/tests/Http/Api/Commands/ApiEntityListEntityStateControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListEntityStateControllerTest.php @@ -7,7 +7,7 @@ use Illuminate\Contracts\Support\Arrayable; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php index 71909f596..65f5915a2 100644 --- a/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListEntityWizardCommandControllerTest.php @@ -7,7 +7,7 @@ use Code16\Sharp\Utils\Fields\FieldsContainer; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php index 266d9974f..58380f96c 100644 --- a/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListInstanceCommandControllerTest.php @@ -10,7 +10,7 @@ use Illuminate\Http\UploadedFile; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php index f023301bd..490ba6870 100644 --- a/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListInstanceWizardCommandControllerTest.php @@ -7,7 +7,7 @@ use Code16\Sharp\Utils\Fields\FieldsContainer; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiEntityListQuickCreationCommandControllerTest.php b/tests/Http/Api/Commands/ApiEntityListQuickCreationCommandControllerTest.php index 1426108c9..63ab2cded 100644 --- a/tests/Http/Api/Commands/ApiEntityListQuickCreationCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiEntityListQuickCreationCommandControllerTest.php @@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Exceptions; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Commands/ApiShowEntityStateControllerTest.php b/tests/Http/Api/Commands/ApiShowEntityStateControllerTest.php index af8d5014d..e47c8e520 100644 --- a/tests/Http/Api/Commands/ApiShowEntityStateControllerTest.php +++ b/tests/Http/Api/Commands/ApiShowEntityStateControllerTest.php @@ -11,9 +11,9 @@ }); it('updates the state of an instance from a show and return a "refresh" action by default', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); - fakeShowFor('person', new class() extends PersonShow + fakeShowFor(PersonEntity::class, new class() extends PersonShow { public function buildShowConfig(): void { @@ -49,9 +49,9 @@ protected function updateState($instanceId, string $stateId): ?array }); it('allows to update the state of an instance from a single show', function () { - sharp()->config()->addEntity('person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); - fakeShowFor('person', new class() extends SinglePersonShow + fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow { public function buildShowConfig(): void { @@ -73,7 +73,7 @@ protected function updateState($instanceId, string $stateId): ?array $this ->postJson( - route('code16.sharp.api.show.state', ['person']), + route('code16.sharp.api.show.state', ['single-person']), [ 'attribute' => 'state', 'value' => 'ok', diff --git a/tests/Http/Api/Commands/ApiShowInstanceCommandControllerTest.php b/tests/Http/Api/Commands/ApiShowInstanceCommandControllerTest.php index b04615091..35338a43f 100644 --- a/tests/Http/Api/Commands/ApiShowInstanceCommandControllerTest.php +++ b/tests/Http/Api/Commands/ApiShowInstanceCommandControllerTest.php @@ -13,9 +13,9 @@ }); it('allows to call an info instance command from a show', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); - fakeShowFor('person', new class() extends PersonShow + fakeShowFor(PersonEntity::class, new class() extends PersonShow { public function getInstanceCommands(): ?array { @@ -45,9 +45,9 @@ public function execute($instanceId, array $data = []): array }); it('allows to call an info instance command from a single show', function () { - sharp()->config()->addEntity('person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); - fakeShowFor('person', new class() extends SinglePersonShow + fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow { public function getInstanceCommands(): ?array { @@ -68,7 +68,7 @@ public function execute($instanceId, array $data = []): array } }); - $this->postJson(route('code16.sharp.api.show.command.instance', ['person', 'cmd'])) + $this->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'cmd'])) ->assertOk() ->assertJson([ 'action' => 'info', @@ -77,9 +77,9 @@ public function execute($instanceId, array $data = []): array }); it('gets form and initialize form data in an instance command of a show', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); - fakeShowFor('person', new class() extends PersonShow + fakeShowFor(PersonEntity::class, new class() extends PersonShow { public function getInstanceCommands(): ?array { @@ -168,9 +168,9 @@ public function execute($instanceId, array $data = []): array }); it('gets form and initialize form data in an instance command of a single show', function () { - sharp()->config()->addEntity('person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); - fakeShowFor('person', new class() extends SinglePersonShow + fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow { public function getInstanceCommands(): ?array { @@ -214,7 +214,7 @@ public function execute($instanceId, array $data = []): array $this ->getJson( route('code16.sharp.api.show.command.singleInstance.form', [ - 'entityKey' => 'person', + 'entityKey' => 'single-person', 'commandKey' => 'single_cmd', ]) ) @@ -257,7 +257,7 @@ public function execute($instanceId, array $data = []): array $this ->postJson( - route('code16.sharp.api.show.command.instance', ['person', 'single_cmd']), + route('code16.sharp.api.show.command.instance', ['single-person', 'single_cmd']), ['data' => ['name' => '']] ) ->assertJsonValidationErrors(['name']); diff --git a/tests/Http/Api/DownloadControllerTest.php b/tests/Http/Api/DownloadControllerTest.php index d6acd9087..910ae6841 100644 --- a/tests/Http/Api/DownloadControllerTest.php +++ b/tests/Http/Api/DownloadControllerTest.php @@ -7,7 +7,7 @@ beforeEach(function () { Storage::fake('local'); - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/Embeds/ApiEmbedsFormControllerTest.php b/tests/Http/Api/Embeds/ApiEmbedsFormControllerTest.php index de3e573d8..be1d48904 100644 --- a/tests/Http/Api/Embeds/ApiEmbedsFormControllerTest.php +++ b/tests/Http/Api/Embeds/ApiEmbedsFormControllerTest.php @@ -6,7 +6,7 @@ use Illuminate\Support\Str; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Api/SearchControllerTest.php b/tests/Http/Api/SearchControllerTest.php index 3c7f7c5ba..f442b98eb 100644 --- a/tests/Http/Api/SearchControllerTest.php +++ b/tests/Http/Api/SearchControllerTest.php @@ -7,7 +7,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); @@ -235,7 +235,7 @@ public function authorize(): bool }); it('the global search is sent with every inertia request, if enabled and authorized', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); $this ->get('/sharp/s-list/person') diff --git a/tests/Http/Auth/AuthenticationTest.php b/tests/Http/Auth/AuthenticationTest.php index f16d588c5..0c8667e0d 100644 --- a/tests/Http/Auth/AuthenticationTest.php +++ b/tests/Http/Auth/AuthenticationTest.php @@ -5,7 +5,7 @@ use Code16\Sharp\Tests\Fixtures\User; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); sharp()->config()->disableImpersonation(); }); diff --git a/tests/Http/Auth/AuthorizationsTest.php b/tests/Http/Auth/AuthorizationsTest.php index 99aecaf92..babcf7288 100644 --- a/tests/Http/Auth/AuthorizationsTest.php +++ b/tests/Http/Auth/AuthorizationsTest.php @@ -15,7 +15,7 @@ beforeEach(function () { login(); - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); sharp()->config()->disableImpersonation(); }); diff --git a/tests/Http/Auth/Login2faNotificationTest.php b/tests/Http/Auth/Login2faNotificationTest.php index ec46aef0c..bf01f6424 100644 --- a/tests/Http/Auth/Login2faNotificationTest.php +++ b/tests/Http/Auth/Login2faNotificationTest.php @@ -10,7 +10,7 @@ beforeEach(function () { auth()->extend('sharp', fn () => new TestAuthGuard()); - sharp()->config()->addEntity('person', PersonEntity::class) + sharp()->config()->declareEntity(PersonEntity::class) ->setAuthCustomGuard('sharp') ->enable2faByNotification(); diff --git a/tests/Http/Auth/Login2faTotpTest.php b/tests/Http/Auth/Login2faTotpTest.php index 4c34786f3..b84573e00 100644 --- a/tests/Http/Auth/Login2faTotpTest.php +++ b/tests/Http/Auth/Login2faTotpTest.php @@ -31,7 +31,7 @@ public function getQRCodeUrl(string $email, string $secret): string ); sharp()->config() - ->addEntity('person', PersonEntity::class) + ->declareEntity(PersonEntity::class) ->setAuthCustomGuard('sharp') ->enable2faCustom(new class(app(Sharp2faTotpEngine::class)) extends Sharp2faTotpHandler { diff --git a/tests/Http/Auth/PolicyAuthorizationsTest.php b/tests/Http/Auth/PolicyAuthorizationsTest.php index 31edc58f6..987f11cb3 100644 --- a/tests/Http/Auth/PolicyAuthorizationsTest.php +++ b/tests/Http/Auth/PolicyAuthorizationsTest.php @@ -11,7 +11,7 @@ beforeEach(function () { login(); - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); }); it('allows to configure a policy', function () { diff --git a/tests/Http/BreadcrumbTest.php b/tests/Http/BreadcrumbTest.php index f19ac489d..760a26e57 100644 --- a/tests/Http/BreadcrumbTest.php +++ b/tests/Http/BreadcrumbTest.php @@ -9,7 +9,7 @@ beforeEach(function () { sharp()->config() ->displayBreadcrumb() - ->addEntity('person', PersonEntity::class); + ->declareEntity(PersonEntity::class); login(); }); @@ -33,19 +33,21 @@ ) ->assertOk(); - expect(sharp()->context()->isShow())->toBeTrue() - ->and(sharp()->context()->breadcrumb()->allSegments())->toHaveCount(2); + expect(sharp()->context()) + ->isShow()->toBeTrue() + ->breadcrumb()->allSegments()->toHaveCount(2); }); it('builds the breadcrumb for a single show page', function () { - sharp()->config()->addEntity('single-person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); $this ->get(route('code16.sharp.single-show', 'single-person')) ->assertOk(); - expect(sharp()->context()->isShow())->toBeTrue() - ->and(sharp()->context()->breadcrumb()->allSegments())->toHaveCount(1); + expect(sharp()->context()) + ->isShow()->toBeTrue() + ->breadcrumb()->allSegments()->toHaveCount(1); }); it('builds the breadcrumb for a form', function () { @@ -59,8 +61,9 @@ ) ->assertOk(); - expect(sharp()->context()->isForm())->toBeTrue() - ->and(sharp()->context()->breadcrumb()->allSegments())->toHaveCount(2); + expect(sharp()->context()) + ->isForm()->toBeTrue() + ->breadcrumb()->allSegments()->toHaveCount(2); }); it('builds the breadcrumb for a form through a show page', function () { @@ -74,8 +77,9 @@ ) ->assertOk(); - expect(sharp()->context()->isForm())->toBeTrue() - ->and(sharp()->context()->breadcrumb()->allSegments())->toHaveCount(3); + expect(sharp()->context()) + ->isForm()->toBeTrue() + ->breadcrumb()->allSegments()->toHaveCount(3); }); it('uses labels defined for entities in the config', function () { diff --git a/tests/Http/Context/SharpContextTest.php b/tests/Http/Context/SharpContextTest.php index ef1f43089..bd2ff8900 100644 --- a/tests/Http/Context/SharpContextTest.php +++ b/tests/Http/Context/SharpContextTest.php @@ -84,6 +84,17 @@ ->previousShowSegment('person')->instanceId()->toEqual(42); }); +it('allows to get previous show of a given entity class name from request', function () { + app(\Code16\Sharp\Config\SharpConfigBuilder::class)->declareEntity(PersonEntity::class); + $this->fakeBreadcrumbWithUrl('/sharp/s-list/person/s-show/person/31/s-show/person/42/s-show/child/84/s-form/child/84'); + + expect(sharp()->context()->breadcrumb()) + ->previousShowSegment()->entityKey()->toBe('child') + ->previousShowSegment()->instanceId()->toEqual(84) + ->previousShowSegment(PersonEntity::class)->entityKey()->toBe('person') + ->previousShowSegment(PersonEntity::class)->instanceId()->toEqual(42); +}); + it('allows to get previous url from request', function () { $this->fakeBreadcrumbWithUrl('/sharp/s-list/person/s-show/person/42/s-form/child/2'); @@ -92,7 +103,7 @@ }); it('allow to retrieve retained filters value in the context', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); fakeListFor('person', new class() extends PersonList diff --git a/tests/Http/DataLocalizationTest.php b/tests/Http/DataLocalizationTest.php index c974e8ac1..f94a5b5f9 100644 --- a/tests/Http/DataLocalizationTest.php +++ b/tests/Http/DataLocalizationTest.php @@ -10,7 +10,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/EntityListControllerTest.php b/tests/Http/EntityListControllerTest.php index 090a9d092..af13d4015 100644 --- a/tests/Http/EntityListControllerTest.php +++ b/tests/Http/EntityListControllerTest.php @@ -15,7 +15,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/ExceptionTest.php b/tests/Http/ExceptionTest.php index 58173ae20..b19461d7f 100644 --- a/tests/Http/ExceptionTest.php +++ b/tests/Http/ExceptionTest.php @@ -7,7 +7,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/FiltersInRequestTest.php b/tests/Http/FiltersInRequestTest.php index 0520b725b..5149d95ae 100644 --- a/tests/Http/FiltersInRequestTest.php +++ b/tests/Http/FiltersInRequestTest.php @@ -9,7 +9,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); diff --git a/tests/Http/Form/FormControllerTest.php b/tests/Http/Form/FormControllerTest.php index f04073981..207c00f7d 100644 --- a/tests/Http/Form/FormControllerTest.php +++ b/tests/Http/Form/FormControllerTest.php @@ -19,7 +19,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); @@ -319,7 +319,7 @@ public function find($id): array }); it('gets form data for an instance in a single form case', function () { - sharp()->config()->addEntity('single-person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); fakeFormFor('single-person', new class() extends PersonSingleForm { @@ -339,7 +339,7 @@ public function findSingle(): array }); it('updates an instance on a single form case', function () { - sharp()->config()->addEntity('single-person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); $this ->post('/sharp/s-show/single-person/s-form/single-person', [ diff --git a/tests/Http/Form/FormEditorUploadsTest.php b/tests/Http/Form/FormEditorUploadsTest.php index ff11cf4b3..ffd6e20eb 100644 --- a/tests/Http/Form/FormEditorUploadsTest.php +++ b/tests/Http/Form/FormEditorUploadsTest.php @@ -16,7 +16,7 @@ beforeEach(function () { $this->withoutExceptionHandling(); - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); Storage::fake('local'); login(); }); diff --git a/tests/Http/Form/HandlesUploadedFilesInRequestTest.php b/tests/Http/Form/HandlesUploadedFilesInRequestTest.php index e0df0af72..d7ec190b3 100644 --- a/tests/Http/Form/HandlesUploadedFilesInRequestTest.php +++ b/tests/Http/Form/HandlesUploadedFilesInRequestTest.php @@ -15,7 +15,7 @@ beforeEach(function () { $this->withoutExceptionHandling(); - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); Storage::fake('local'); Queue::fake(); diff --git a/tests/Http/GlobalFilterControllerTest.php b/tests/Http/GlobalFilterControllerTest.php index 5b1b02e95..bdbae6568 100644 --- a/tests/Http/GlobalFilterControllerTest.php +++ b/tests/Http/GlobalFilterControllerTest.php @@ -59,7 +59,7 @@ public function defaultValue(): mixed }); it('the current value of the global filter is sent with every inertia request', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); $this ->get('/sharp/s-list/person') diff --git a/tests/Http/ShowControllerTest.php b/tests/Http/ShowControllerTest.php index 21aea7750..6e5c5fc19 100644 --- a/tests/Http/ShowControllerTest.php +++ b/tests/Http/ShowControllerTest.php @@ -17,7 +17,7 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); login(); }); @@ -197,7 +197,7 @@ public function buildShowConfig(): void }); it('gets show data for an instance in a single show case', function () { - sharp()->config()->addEntity('single-person', SinglePersonEntity::class); + sharp()->config()->declareEntity(SinglePersonEntity::class); fakeShowFor('single-person', new class() extends PersonSingleShow { diff --git a/tests/Pest.php b/tests/Pest.php index 5bbde429f..9449e308d 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -2,6 +2,7 @@ use Code16\Sharp\Tests\Fixtures\User; use Code16\Sharp\Tests\TestCase; +use Code16\Sharp\Utils\Entities\SharpEntityManager; use Illuminate\Database\Schema\Blueprint; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Schema; @@ -74,37 +75,53 @@ function login(?User $user = null) ); } -function fakeListFor(string $entityKey, $fakeImplementation) +function fakeListFor(string $entityKeyOrClass, $fakeImplementation) { - app(\Code16\Sharp\Utils\Entities\SharpEntityManager::class) - ->entityFor($entityKey) + app(SharpEntityManager::class) + ->entityFor( + class_exists($entityKeyOrClass) + ? $entityKeyOrClass::$entityKey + : $entityKeyOrClass + ) ->setList($fakeImplementation); return test(); } -function fakeShowFor(string $entityKey, $fakeImplementation) +function fakeShowFor(string $entityKeyOrClass, $fakeImplementation) { - app(\Code16\Sharp\Utils\Entities\SharpEntityManager::class) - ->entityFor($entityKey) + app(SharpEntityManager::class) + ->entityFor( + class_exists($entityKeyOrClass) + ? ($entityKeyOrClass::$entityKey ?? null) + : $entityKeyOrClass + ) ->setShow($fakeImplementation); return test(); } -function fakeFormFor(string $entityKey, $fakeImplementation) +function fakeFormFor(string $entityKeyOrClass, $fakeImplementation) { - app(\Code16\Sharp\Utils\Entities\SharpEntityManager::class) - ->entityFor($entityKey) + app(SharpEntityManager::class) + ->entityFor( + class_exists($entityKeyOrClass) + ? $entityKeyOrClass::$entityKey + : $entityKeyOrClass + ) ->setForm($fakeImplementation); return test(); } -function fakePolicyFor(string $entityKey, $fakeImplementation) +function fakePolicyFor(string $entityKeyOrClass, $fakeImplementation) { - app(\Code16\Sharp\Utils\Entities\SharpEntityManager::class) - ->entityFor($entityKey) + app(SharpEntityManager::class) + ->entityFor( + class_exists($entityKeyOrClass) + ? $entityKeyOrClass::$entityKey + : $entityKeyOrClass + ) ->setPolicy($fakeImplementation); return test(); diff --git a/tests/Unit/Config/SharpConfigBuilderTest.php b/tests/Unit/Config/SharpConfigBuilderTest.php index 6e6c95421..94425bc4e 100644 --- a/tests/Unit/Config/SharpConfigBuilderTest.php +++ b/tests/Unit/Config/SharpConfigBuilderTest.php @@ -1,9 +1,29 @@ config()->setName('Test project') ->setCustomUrlSegment('test-sharp'); - expect(sharp()->config()->get('name'))->toBe('Test project') - ->and(sharp()->config()->get('custom_url_segment'))->toBe('test-sharp'); + expect(sharp()->config())->get('name')->toBe('Test project') + ->get('custom_url_segment')->toBe('test-sharp'); +}); + +it('allows to declare an entity without entity key', function () { + class WithoutEntityKeyEntity extends SharpEntity {} + sharp()->config()->declareEntity(WithoutEntityKeyEntity::class); + + expect(sharp()->config()->get('entities'))->toHaveKey('without-entity-key'); +}); + +it('allows to declare an entity with an entity key', function () { + class WithEntityKeyEntity extends SharpEntity + { + public static string $entityKey = 'my-entity'; + } + + sharp()->config()->declareEntity(WithEntityKeyEntity::class); + + expect(sharp()->config()->get('entities'))->toHaveKey('my-entity'); }); diff --git a/tests/Unit/Utils/SharpEntityManagerTest.php b/tests/Unit/Utils/SharpEntityManagerTest.php index 4e9c0fd17..e18f9081d 100644 --- a/tests/Unit/Utils/SharpEntityManagerTest.php +++ b/tests/Unit/Utils/SharpEntityManagerTest.php @@ -7,12 +7,19 @@ use Code16\Sharp\Utils\Entities\SharpEntityResolver; it('returns an entity declared in configuration', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); expect(app(SharpEntityManager::class)->entityFor('person')) ->toBeInstanceOf(PersonEntity::class); }); +it('returns an entity declared in the deprecated way in configuration', function () { + sharp()->config()->addEntity('another-person', PersonEntity::class); + + expect(app(SharpEntityManager::class)->entityFor('another-person')) + ->toBeInstanceOf(PersonEntity::class); +}); + it('throws an exception on unknown entity', function () { app(SharpEntityManager::class)->entityFor('person'); })->throws(SharpInvalidEntityKeyException::class); @@ -23,17 +30,17 @@ { public function entityClassName(string $entityKey): ?string { - return $entityKey == 'person' ? PersonEntity::class : null; + return $entityKey == 'a-person' ? PersonEntity::class : null; } } ); - expect(app(SharpEntityManager::class)->entityFor('person')) + expect(app(SharpEntityManager::class)->entityFor('a-person')) ->toBeInstanceOf(PersonEntity::class); }); it('returns an entity key for an entity class or instance', function () { - sharp()->config()->addEntity('person', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); expect(app(SharpEntityManager::class)) ->entityKeyFor(PersonEntity::class)->toEqual('person') diff --git a/tests/Unit/Utils/SharpLinkToTest.php b/tests/Unit/Utils/SharpLinkToTest.php index 42aac59e6..d55f4db58 100644 --- a/tests/Unit/Utils/SharpLinkToTest.php +++ b/tests/Unit/Utils/SharpLinkToTest.php @@ -63,7 +63,7 @@ it('allows to generate a link to a show page passing a SharpEntity class', function () { app(SharpConfigBuilder::class) - ->addEntity('person', PersonEntity::class); + ->declareEntity(PersonEntity::class); $this->assertEquals( 'test', @@ -86,7 +86,7 @@ it('allows to generate an url to a show page with a specific breadcrumb passing a SharpEntity class', function () { app(SharpConfigBuilder::class) - ->addEntity('person', PersonEntity::class); + ->declareEntity(PersonEntity::class); $this->assertEquals( 'http://localhost/sharp/s-list/person/s-show/person/3/s-show/person/4',