Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Sharp
https://github.com/code16/sharp
Admin
Sharp is a code-driven content management framework built as a Laravel package, offering a clean UI
...
Tokens:
108,503
Snippets:
825
Trust Score:
8.1
Update:
4 months ago
Context
Skills
Chat
Benchmark
94.2
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Sharp - Laravel Content Management Framework ## Introduction Sharp is a content management framework built for Laravel as a package, providing a clean and intuitive UI for building CMS sections in projects. Developed and maintained by Code16, Sharp is driven by code rather than configuration, offering a documented PHP API that follows Laravel conventions and coding style. The framework is designed to be data-agnostic with no expectations from the persistence layer, avoiding code adherence so the main project has no knowledge of Sharp's existence. It's built with modern technologies including Inertia.js, Vue.js, Tailwind CSS, and requires Laravel 11+ with PHP 8.3+. Sharp provides comprehensive solutions for creating, updating, and deleting structured data with validation and error handling, displaying and filtering data through entity lists, executing custom commands on instances or selections, handling authorizations, and managing complex forms with rich content editors. The framework includes built-in support for multi-language content, file uploads with automatic image optimization, dashboards with various widget types, and a flexible authorization system. All functionality is accessible through a clean PHP API without writing frontend code, making it ideal for content-driven websites, e-commerce platforms, and API backends. ## Form Creation Building forms with SharpForm SharpForm is the base class for creating forms to edit or create entity instances. Forms define fields, layout, validation rules, and data transformation logic. ```php <?php namespace App\Sharp; use App\Models\Post; use Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater; use Code16\Sharp\Form\Fields\SharpFormTextField; use Code16\Sharp\Form\Fields\SharpFormEditorField; use Code16\Sharp\Form\Fields\SharpFormUploadField; use Code16\Sharp\Form\Fields\SharpFormTagsField; use Code16\Sharp\Form\Fields\SharpFormDateField; use Code16\Sharp\Form\Layout\FormLayout; use Code16\Sharp\Form\Layout\FormLayoutColumn; use Code16\Sharp\Form\Layout\FormLayoutTab; use Code16\Sharp\Form\SharpForm; use Code16\Sharp\Utils\Fields\FieldsContainer; use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer; class PostForm extends SharpForm { use WithSharpFormEloquentUpdater; public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormTextField::make('title') ->setLabel('Title') ->setLocalized() ->setMaxLength(150) ) ->addField( SharpFormEditorField::make('content') ->setLabel('Post content') ->setLocalized() ->setToolbar([ SharpFormEditorField::H2, SharpFormEditorField::B, SharpFormEditorField::UL, SharpFormEditorField::A, SharpFormEditorField::QUOTE, ]) ->setMaxLength(2000) ) ->addField( SharpFormUploadField::make('cover') ->setMaxFileSize(1) ->setImageOnly() ->setImageCropRatio('16:9') ->setStorageDisk('local') ->setStorageBasePath('data/posts/{id}') ) ->addField( SharpFormTagsField::make('categories', Category::pluck('name', 'id')->toArray()) ->setLabel('Categories') ->setCreatable() ->setCreateAttribute('name') ) ->addField( SharpFormDateField::make('published_at') ->setLabel('Publication date') ->setHasTime() ); } public function buildFormLayout(FormLayout $formLayout): void { $formLayout ->addTab('Content', function (FormLayoutTab $tab) { $tab->addColumn(6, function (FormLayoutColumn $column) { $column ->withField('title') ->withFields('published_at', 'categories'); }) ->addColumn(6, function (FormLayoutColumn $column) { $column ->withField('cover') ->withField('content'); }); }); } public function buildFormConfig(): void { $this->configureDisplayShowPageAfterCreation() ->configureEditTitle('Edit post') ->configureCreateTitle('New post'); } public function find($id): array { return $this ->setCustomTransformer('cover', new SharpUploadModelFormAttributeTransformer()) ->transform(Post::with('categories')->findOrFail($id)); } public function rules(): array { return [ 'title.en' => ['required', 'string', 'max:150'], 'title.fr' => ['required', 'string', 'max:150'], 'content.en' => ['nullable', 'string', 'max:2000'], 'published_at' => ['required', 'date'], ]; } public function update($id, array $data) { $post = $id ? Post::findOrFail($id) : new Post(['author_id' => auth()->id()]); $this->save($post, $data); if (sharp()->context()->isCreation()) { $this->notify('Your post was created successfully.'); } return $post->id; } public function getDataLocalizations(): array { return ['en', 'fr']; } } ``` ## Entity List Creation Building entity lists with SharpEntityList SharpEntityList provides the base class for creating searchable, filterable, sortable lists of entities with bulk actions and quick creation capabilities. ```php <?php namespace App\Sharp; use App\Models\Category; use App\Sharp\Commands\CleanUnusedCategoriesCommand; use Code16\Sharp\EntityList\Eloquent\SimpleEloquentReorderHandler; use Code16\Sharp\EntityList\Fields\EntityListField; use Code16\Sharp\EntityList\Fields\EntityListFieldsContainer; use Code16\Sharp\EntityList\SharpEntityList; use Code16\Sharp\Filters\CheckFilter; class CategoryList extends SharpEntityList { protected function buildList(EntityListFieldsContainer $fields): void { $fields ->addField( EntityListField::make('name') ->setLabel('Name') ->setSortable() ) ->addField( EntityListField::make('posts_count') ->setLabel('# posts') ->setWidth(0.2) ); } public function buildListConfig(): void { $this ->configureSearchable() ->configureReorderable(new SimpleEloquentReorderHandler(Category::class)) ->configureDefaultSort('created_at', 'desc') ->configureQuickCreationForm(['name']); } protected function getEntityCommands(): ?array { return [ CleanUnusedCategoriesCommand::class, ]; } protected function getFilters(): ?array { return [ new class() extends CheckFilter { public function buildFilterConfig(): void { $this->configureKey('orphan') ->configureLabel('Orphan categories only'); } }, ]; } public function getListData(): array|Arrayable { $categories = Category::withCount('posts') ->orderBy('order') ->when( $this->queryParams->filterFor('orphan'), fn ($q) => $q->having('posts_count', 0) ) ->when( $this->queryParams->hasSearch(), fn ($q) => $q->where('name', 'like', '%' . $this->queryParams->searchWords()[0] . '%') ) ->when( $this->queryParams->sortedBy(), fn ($q) => $q->orderBy( $this->queryParams->sortedBy(), $this->queryParams->sortedDir() ) ); return $this->transform($categories->get()); } public function delete(mixed $id): void { Category::findOrFail($id)->delete(); } } ``` ## Show Page Creation Building show pages with SharpShow SharpShow creates detailed view pages for displaying entity instances with support for embedded lists, dashboards, and custom transformers. ```php <?php namespace App\Sharp; use App\Models\Post; use App\Sharp\Entities\PostEntity; use App\Sharp\Commands\PreviewPostCommand; use Code16\Sharp\Show\Fields\SharpShowTextField; use Code16\Sharp\Show\Fields\SharpShowPictureField; use Code16\Sharp\Show\Fields\SharpShowListField; use Code16\Sharp\Show\Fields\SharpShowFileField; use Code16\Sharp\Show\Layout\ShowLayout; use Code16\Sharp\Show\Layout\ShowLayoutColumn; use Code16\Sharp\Show\Layout\ShowLayoutSection; use Code16\Sharp\Show\SharpShow; use Code16\Sharp\Utils\Fields\FieldsContainer; use Code16\Sharp\Utils\Links\LinkToEntityList; use Code16\Sharp\Utils\Transformers\Attributes\Eloquent\SharpUploadModelThumbnailUrlTransformer; use Code16\Sharp\Utils\PageAlerts\PageAlert; class PostShow extends SharpShow { protected function buildShowFields(FieldsContainer $showFields): void { $showFields ->addField( SharpShowTextField::make('content') ->setLocalized() ->collapseToWordCount(40) ) ->addField(SharpShowTextField::make('author')->setLabel('Author')) ->addField(SharpShowTextField::make('categories')->setLabel('Categories')) ->addField(SharpShowPictureField::make('cover')) ->addField( SharpShowListField::make('attachments') ->setLabel('Attachments') ->addItemField(SharpShowTextField::make('title')->setLabel('Title')) ->addItemField(SharpShowTextField::make('link_url')->setLabel('External link')) ->addItemField(SharpShowFileField::make('document')->setLabel('File')) ); } protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection('Details', function (ShowLayoutSection $section) { $section ->addColumn(7, function (ShowLayoutColumn $column) { $column ->withFields(categories: 5, author: 7) ->withListField('attachments', function (ShowLayoutColumn $item) { $item->withFields(title: 6, link_url: 6, document: 6); }); }) ->addColumn(5, function (ShowLayoutColumn $column) { $column->withField('cover'); }); }) ->addSection('Content', function (ShowLayoutSection $section) { $section ->setKey('content-section') ->addColumn(8, function (ShowLayoutColumn $column) { $column->withField('content'); }); }); } public function buildShowConfig(): void { $this ->configurePageTitleAttribute('title', localized: true) ->configureDeleteConfirmationText('Are you sure you want to delete this post?'); } protected function buildPageAlert(PageAlert $pageAlert): void { $pageAlert ->setLevelInfo() ->setMessage(fn (array $data) => $data['is_published'] ? 'This post is currently published' : 'This post is in draft mode' ); } public function getInstanceCommands(): ?array { return [ 'content-section' => [PreviewPostCommand::class], ]; } public function find(mixed $id): array { $post = Post::with('attachments', 'categories', 'author')->findOrFail($id); return $this ->setCustomTransformer('author', fn ($value, $instance) => $instance->author ? $instance->author->name : 'Unknown' ) ->setCustomTransformer('categories', fn ($value, $instance) => $instance->categories->pluck('name')->join(', ') ) ->setCustomTransformer('cover', new SharpUploadModelThumbnailUrlTransformer(500)) ->transform($post); } public function delete(mixed $id): void { Post::findOrFail($id)->delete(); } } ``` ## Dashboard Creation Building dashboards with SharpDashboard SharpDashboard provides widget-based dashboards with graphs, figures, lists, and custom panels for data visualization. ```php <?php namespace App\Sharp; use App\Models\Post; use App\Models\Category; use Code16\Sharp\Dashboard\Layout\DashboardLayout; use Code16\Sharp\Dashboard\Layout\DashboardLayoutRow; use Code16\Sharp\Dashboard\Layout\DashboardLayoutSection; use Code16\Sharp\Dashboard\SharpDashboard; use Code16\Sharp\Dashboard\Widgets\SharpBarGraphWidget; use Code16\Sharp\Dashboard\Widgets\SharpPieGraphWidget; use Code16\Sharp\Dashboard\Widgets\SharpLineGraphWidget; use Code16\Sharp\Dashboard\Widgets\SharpFigureWidget; use Code16\Sharp\Dashboard\Widgets\SharpOrderedListWidget; use Code16\Sharp\Dashboard\Widgets\SharpGraphWidgetDataSet; use Code16\Sharp\Dashboard\Widgets\WidgetsContainer; class PostsDashboard extends SharpDashboard { protected function buildWidgets(WidgetsContainer $widgetsContainer): void { $widgetsContainer ->addWidget( SharpBarGraphWidget::make('posts_by_author') ->setTitle('Posts by author') ->setShowLegend(false) ->setHorizontal() ) ->addWidget( SharpPieGraphWidget::make('posts_by_category') ->setTitle('Posts by category') ) ->addWidget( SharpLineGraphWidget::make('visits') ->setTitle('Monthly visits') ->setHeight(200) ->setShowLegend() ->setCurvedLines() ) ->addWidget( SharpFigureWidget::make('draft_count') ->setTitle('Draft posts') ) ->addWidget( SharpFigureWidget::make('published_count') ->setTitle('Published posts') ) ->addWidget( SharpOrderedListWidget::make('top_categories') ->setTitle('Top 5 categories') ); } protected function buildDashboardLayout(DashboardLayout $dashboardLayout): void { $dashboardLayout ->addSection('Overview', function (DashboardLayoutSection $section) { $section->addRow(function (DashboardLayoutRow $row) { $row->addWidget(6, 'draft_count') ->addWidget(6, 'published_count'); }); }) ->addSection('Statistics', function (DashboardLayoutSection $section) { $section ->addRow(fn (DashboardLayoutRow $row) => $row ->addWidget(6, 'posts_by_author') ->addWidget(6, 'posts_by_category') ) ->addFullWidthWidget('visits') ->addRow(fn (DashboardLayoutRow $row) => $row ->addWidget(12, 'top_categories') ); }); } protected function buildWidgetsData(): void { // Figure widgets $this->setFigureData( 'draft_count', figure: Post::where('state', 'draft')->count(), evolution: '+15%' ); $this->setFigureData( 'published_count', figure: Post::where('state', 'published')->count(), unit: 'post(s)', evolution: '-10%' ); // Bar graph $authorData = User::withCount('posts') ->orderBy('posts_count', 'desc') ->limit(5) ->get() ->pluck('posts_count', 'name'); $this->addGraphDataSet( 'posts_by_author', SharpGraphWidgetDataSet::make($authorData) ->setColor('#2a9d90') ); // Pie graph Category::withCount('posts') ->orderBy('posts_count', 'desc') ->limit(5) ->get() ->each(function (Category $category) { $this->addGraphDataSet( 'posts_by_category', SharpGraphWidgetDataSet::make([$category->posts_count]) ->setLabel($category->name) ->setColor('#' . dechex(rand(0x000000, 0xFFFFFF))) ); }); // Line graph with multiple datasets $visits = ['Jan' => 1000, 'Feb' => 1200, 'Mar' => 1100, 'Apr' => 1400]; $this->addGraphDataSet( 'visits', SharpGraphWidgetDataSet::make($visits) ->setLabel('Total visits') ->setColor('#2a9d90') ); // Ordered list $this->setOrderedListData( 'top_categories', Category::withCount('posts') ->orderBy('posts_count', 'desc') ->limit(5) ->get() ->map(fn (Category $cat) => [ 'label' => $cat->name, 'count' => $cat->posts_count, ]) ->toArray() ); } } ``` ## Command Creation Building entity and instance commands Commands execute actions on entities or instances with optional forms and various return types for user feedback. ```php <?php namespace App\Sharp\Commands; use App\Models\Category; use Code16\Sharp\EntityList\Commands\EntityCommand; use Code16\Sharp\Exceptions\Form\SharpApplicativeException; // Entity Command: acts on selection or all items class CleanUnusedCategoriesCommand extends EntityCommand { public function label(): ?string { return 'Clean unused categories...'; } public function buildCommandConfig(): void { $this->configureConfirmationText( 'Delete all categories without post attached?', title: 'Are you sure?' )->configureDescription('This action will remove all orphan categories'); } public function execute(array $data = []): array { $deletedCount = Category::whereDoesntHave('posts')->delete(); if ($deletedCount === 0) { throw new SharpApplicativeException('No unused category found!'); } $this->notify($deletedCount . ' categories were deleted!') ->setLevelInfo(); return $this->reload(); } } // Instance Command with form: acts on specific instance namespace App\Sharp\Commands; use App\Models\User; use Code16\Sharp\EntityList\Commands\EntityCommand; use Code16\Sharp\Form\Fields\SharpFormTextField; use Code16\Sharp\Utils\Fields\FieldsContainer; class InviteUserCommand extends EntityCommand { public function label(): ?string { return 'Invite new author...'; } public function buildCommandConfig(): void { $this->configureFormModalTitle('Invite a new user as author') ->configureFormModalDescription('Provide email and name for the new author.') ->configureFormModalButtonLabel('Send invitation') ->configureIcon('lucide-user-plus'); } public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField(SharpFormTextField::make('email')->setLabel('Email')) ->addField(SharpFormTextField::make('name')->setLabel('Name')); } public function execute(array $data = []): array { $this->validate($data, [ 'email' => ['required', 'email', 'max:100', 'unique:users,email'], 'name' => ['required', 'string', 'max:100'], ]); $user = User::create([ 'email' => $data['email'], 'name' => $data['name'], 'password' => bcrypt(Str::random()), ]); // Return types available: // return $this->reload(); // return $this->refresh([$user->id]); // return $this->info('Success message', reload: true); // return $this->link('/some-url', openInNewTab: true); // return $this->download($filePath, $fileName); // return $this->view('blade.view', ['data' => $value]); return $this->info('Invitation sent!', reload: true); } } ``` ## Configuration and Setup Sharp application configuration Configure Sharp's menu, authentication, uploads, theme, and entity registration through the service provider or config file. ```php <?php // config/sharp.php or in AppServiceProvider use Code16\Sharp\Config\SharpConfigBuilder; use Code16\Sharp\Utils\Menu\SharpMenu; use App\Sharp\Entities\PostEntity; use App\Sharp\Entities\CategoryEntity; return (new SharpConfigBuilder()) // Application name and URL ->setName('My CMS') ->setCustomUrlSegment('admin') // Entity registration ->declareEntity(PostEntity::class) ->declareEntity(CategoryEntity::class) // Or auto-discover: ->discoverEntities(['Sharp/Entities']) // Menu configuration ->setSharpMenu(MySharpMenu::class) // Search ->enableGlobalSearch(MySearchEngine::class, 'Search...') // Authentication ->setLoginAttributes('email', 'password') ->setUserDisplayAttribute('name') ->setUserAvatarAttribute('avatar_url') ->enableImpersonation() ->enableForgottenPassword() ->enable2faByNotification() // Uploads configuration ->configureUploads('local', 'tmp', 5) // disk, path, max size in MB ->configureUploadsThumbnailCreation('public', 'thumbnails') // Theme customization ->setThemeColor('#004c9b') ->setThemeLogo('/logo.png', '2rem', '/favicon.ico') // Middleware ->appendToMiddleware(MyMiddleware::class) ->get(); // Menu configuration class class MySharpMenu extends SharpMenu { public function build(): self { return $this ->setUserMenu(function (SharpMenuUserMenu $userMenu) { $userMenu ->addEntityLink(ProfileEntity::class, 'My Profile', 'lucide-user') ->addExternalLink('https://docs.example.com', 'Documentation'); }) ->addSection('Content', function (SharpMenuItemSection $section) { $section ->setCollapsible(false) ->addEntityLink(PostEntity::class, 'Posts', 'lucide-file-text') ->addEntityLink(CategoryEntity::class, 'Categories', 'lucide-tags'); }) ->addEntityLink(DashboardEntity::class, 'Dashboard', 'lucide-layout-dashboard'); } } // Entity configuration use Code16\Sharp\Utils\Entities\SharpEntity; class PostEntity extends SharpEntity { protected ?string $list = PostList::class; protected ?string $form = PostForm::class; protected ?string $show = PostShow::class; protected ?string $policy = PostPolicy::class; protected string $label = 'Posts'; } ``` ## Advanced Form Features Complex form fields and conditional display Sharp provides advanced form fields including repeatable lists, autocomplete, conditional display, and live refresh capabilities. ```php <?php use Code16\Sharp\Form\Fields\SharpFormListField; use Code16\Sharp\Form\Fields\SharpFormAutocompleteRemoteField; use Code16\Sharp\Form\Fields\SharpFormHtmlField; use Code16\Sharp\Form\Fields\SharpFormCheckField; use Code16\Sharp\Form\Fields\SharpFormUploadField; // Repeatable list field with conditional display $formFields->addField( SharpFormListField::make('attachments') ->setLabel('Attachments') ->setAddable()->setAddText('Add attachment') ->setRemovable() ->setMaxItemCount(5) ->setSortable()->setOrderAttribute('order') ->allowBulkUploadForField('document') ->addItemField( SharpFormTextField::make('title')->setLabel('Title') ) ->addItemField( SharpFormCheckField::make('is_link', 'It's a link') ) ->addItemField( SharpFormTextField::make('link_url') ->setPlaceholder('URL of the link') ->addConditionalDisplay('is_link') // Show only when is_link is true ) ->addItemField( SharpFormUploadField::make('document') ->setMaxFileSize(2) ->setAllowedExtensions(['pdf', 'zip', 'mp4']) ->setStorageDisk('local') ->setStorageBasePath('data/posts/{id}') ->addConditionalDisplay('!is_link') // Show only when is_link is false ) ); // Remote autocomplete with search callback $formFields->addField( SharpFormAutocompleteRemoteField::make('author_id') ->setLabel('Author') ->setReadOnly(!auth()->user()->isAdmin()) ->allowEmptySearch() ->setRemoteCallback(function ($search) { $users = User::orderBy('name')->limit(10); foreach (explode(' ', trim($search)) as $word) { $users->where(fn ($query) => $query ->where('name', 'like', "%$word%") ->orWhere('email', 'like', "%$word%") ); } return $users->get(); }) ->setListItemTemplate('<div>{{ $name }}</div><div><small>{{ $email }}</small></div>') ->setHelpMessage('This field is only editable by admins.') ); // Live refresh HTML field $formFields->addField( SharpFormHtmlField::make('summary') ->setLiveRefresh(linkedFields: ['author_id', 'published_at', 'categories']) ->setTemplate(function (array $data) { if (!isset($data['published_at'])) { return ''; } return Blade::render(<<<'blade' This post will be published on {{ $published_at }} @if($author) by {{ $author->name }}. @endif Categories: {{ $categories }} blade, [ 'published_at' => Carbon::parse($data['published_at'])->isoFormat('LLLL'), 'author' => isset($data['author_id']) ? User::find($data['author_id']) : null, 'categories' => implode(', ', $data['categories'] ?? []), ]); }) ); // Rich text editor with uploads and embeds $formFields->addField( SharpFormEditorField::make('content') ->setLocalized() ->setToolbar([ SharpFormEditorField::H2, SharpFormEditorField::B, SharpFormEditorField::I, SharpFormEditorField::UL, SharpFormEditorField::A, SharpFormEditorField::QUOTE, SharpFormEditorField::SEPARATOR, SharpFormEditorField::IFRAME, SharpFormEditorField::UPLOAD, ]) ->allowUploads( SharpFormEditorUpload::make() ->setStorageDisk('local') ->setStorageBasePath('data/posts/{id}/embed') ->setMaxFileSize(2) ->setHasLegend() ) ->setTextInputReplacements([ new EditorTextInputReplacement('/:\+1:/', '👍'), new EditorTextInputReplacement('/(c)/', '©'), ]) ->allowFullscreen() ->setMaxLength(2000) ->setHeight(300, 600) ); ``` ## Data Transformers and Helpers Custom transformers and Sharp utilities Sharp provides transformers for converting model data to form-compatible formats and utility functions for context-aware operations. ```php <?php use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer; use Code16\Sharp\Utils\Transformers\Attributes\Eloquent\SharpUploadModelThumbnailUrlTransformer; use Code16\Sharp\Utils\Transformers\Attributes\Eloquent\SharpTagsTransformer; use Code16\Sharp\Utils\Links\LinkToEntityList; // In Form find() method - transform model to form data public function find($id): array { $post = Post::with('cover', 'attachments', 'categories', 'author')->findOrFail($id); return $this // Transform upload for form display ->setCustomTransformer('cover', new SharpUploadModelFormAttributeTransformer()) // Transform nested uploads with playable preview ->setCustomTransformer( 'attachments[document]', new SharpUploadModelFormAttributeTransformer(withPlayablePreview: true) ) // Custom transformer with closure ->setCustomTransformer('author_id', fn ($value, Post $instance) => $instance->author ? ['id' => $instance->author->id, 'label' => $instance->author->name] : null ) ->transform($post); } // In Show find() method - transform model for display public function find(mixed $id): array { $post = Post::with('categories', 'author')->findOrFail($id); return $this // Transform upload to thumbnail URL ->setCustomTransformer('cover', new SharpUploadModelThumbnailUrlTransformer(500)) // Transform tags with clickable filter links ->setCustomTransformer( 'categories', new SharpTagsTransformer('name')->setFilterLink(PostEntity::class, CategoryFilter::class) ) // Create link to entity list with search ->setCustomTransformer('author', fn ($value, $instance) => $instance->author_id ? LinkToEntityList::make(AuthorEntity::class) ->setSearch($instance->author->email) ->renderAsText($instance->author->name) : null ) // Format HTML output ->setCustomTransformer('link_url', fn ($value, $instance) => $instance->is_link ? sprintf('<a href="%s">%s</a>', $value, str($value)->limit(30)) : null ) ->transform($post); } // Sharp context utilities if (sharp()->context()->isCreation()) { // In form creation mode $this->notify('Draft saved! Publish when ready.'); } if (sharp()->context()->isUpdate()) { // In form update mode $post = Post::findOrFail($id); } if (sharp()->context()->isShow()) { // In show page mode } // Get instance from list context $instance = sharp()->context()->findListInstance($id, function ($id) { return Post::findOrFail($id); }); // WithSharpFormEloquentUpdater trait for automatic saving use Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater; class PostForm extends SharpForm { use WithSharpFormEloquentUpdater; public function update($id, array $data) { $post = $id ? Post::findOrFail($id) : new Post(['author_id' => auth()->id()]); // Automatically saves model with relationships // Ignores specific fields if needed $this ->ignore(auth()->user()->isAdmin() ? [] : ['author_id']) ->save($post, $data); return $post->id; } } ``` ## Filters and Authorization Entity list filters and policy integration Sharp supports various filter types for entity lists and integrates with Laravel policies for authorization. ```php <?php use Code16\Sharp\Filters\SelectFilter; use Code16\Sharp\Filters\SelectMultipleFilter; use Code16\Sharp\Filters\DateRangeFilter; use Code16\Sharp\Filters\CheckFilter; // Filter implementations class CategoryFilter extends SelectMultipleFilter { public function buildFilterConfig(): void { $this->configureLabel('Category') ->configureKey('category') ->configureSearchable(); } public function values(): array { return Category::orderBy('name') ->pluck('name', 'id') ->toArray(); } } class StateFilter extends SelectFilter { public function buildFilterConfig(): void { $this->configureLabel('Status') ->configureKey('state'); } public function values(): array { return [ 'draft' => 'Draft', 'online' => 'Published', ]; } public function defaultValue() { return 'online'; } } class PeriodFilter extends DateRangeFilter { public function buildFilterConfig(): void { $this->configureLabel('Publication period') ->configureKey('period') ->configureMondayFirst(); } } // Using filters in entity list class PostList extends SharpEntityList { protected function getFilters(): ?array { return [ StateFilter::class, CategoryFilter::class, PeriodFilter::class, ]; } public function getListData(): array|Arrayable { $posts = Post::with('author', 'categories'); // Apply state filter if ($state = $this->queryParams->filterFor(StateFilter::class)) { $posts->where('state', $state); } // Apply category filter (multiple values) if ($categories = $this->queryParams->filterFor(CategoryFilter::class)) { $posts->whereHas('categories', fn ($q) => $q->whereIn('id', $categories)); } // Apply date range filter if ($period = $this->queryParams->filterFor(PeriodFilter::class)) { $posts->whereBetween('published_at', [ $period['start'], $period['end'] ]); } return $this->transform($posts->get()); } } // Authorization with Laravel policies class PostPolicy { public function view(User $user, Post $post): bool { return $user->isAdmin() || $user->id === $post->author_id; } public function update(User $user, Post $post): bool { return $user->isAdmin() || $user->id === $post->author_id; } public function delete(User $user, Post $post): bool { return $user->isAdmin(); } public function create(User $user): bool { return $user->role === 'editor' || $user->role === 'admin'; } } // Link policy to entity class PostEntity extends SharpEntity { protected ?string $policy = PostPolicy::class; protected ?string $form = PostForm::class; protected ?string $list = PostList::class; protected string $label = 'Posts'; } ``` ## Summary Sharp is a comprehensive content management framework for Laravel that eliminates the need for custom admin panel development while maintaining complete flexibility through code-driven configuration. The framework excels at managing structured data through intuitive forms with rich field types including text, dates, uploads, tags, repeatable lists, and a powerful Tiptap-based rich text editor with embed support. Entity lists provide searchable, sortable, filterable views with bulk actions and quick creation capabilities. Show pages display detailed entity views with embedded lists and dashboards. The dashboard system offers multiple widget types (graphs, figures, panels, ordered lists) for data visualization. Commands enable custom actions on entities or instances with optional forms and various response types (reload, redirect, download, notifications). Sharp integrates seamlessly with Laravel's ecosystem, supporting Eloquent models with automatic relationship handling, Laravel policies for authorization, validation, localization, and middleware. The framework provides built-in transformers for common data operations like upload handling and thumbnail generation, while allowing custom transformers for specialized needs. Configuration is centralized through a fluent API covering authentication, uploads, theming, menu structure, and entity registration. Sharp's data-agnostic architecture means the main application remains unaware of Sharp's presence, avoiding code adherence. With support for multi-language content, file uploads with image optimization, entity states, reordering, and conditional field display, Sharp handles complex CMS requirements while maintaining clean, maintainable code through its documented PHP API following Laravel conventions.