tobento / app-block
App block editor.
Requires
- php: >=8.0
- tobento/app: ^1.0.7
- tobento/app-console: ^1.0
- tobento/app-crud: ^1.0
- tobento/app-http: ^1.0
- tobento/app-migration: ^1.0
- tobento/app-user: ^1.0
- tobento/app-view: ^1.0
Requires (Dev)
- nyholm/psr7: ^1.4
- phpunit/phpunit: ^9.5
- tobento/app-mail: ^1.0
- tobento/app-profiler: ^1.0
- tobento/app-testing: ^1.0
- tobento/apps: ^1.0
- vimeo/psalm: ^4.0
README
The app block provides interfaces to create block editors. There are two editors available, but you can easily create your custom editor.
Editing blocks is kept simple having clients in minds. Furthermore, blocks use CSS classes only to style its content. This has multiple advantages:
- easily customize content by its classes
- using strong Content-Security-Policy blocking style-src
- limits user to keep corporate design
Table of Contents
- Getting Started
- Documentation
- Credits
Getting Started
Add the latest version of the app block project running this command.
composer require tobento/app-block
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Block Boot
The block boot does the following:
- installs and loads block config file
- implements needed interfaces
- add routes for the block editors
use Tobento\App\AppFactory; use Tobento\App\Block\BlockRepositoryInterface; use Tobento\App\Block\EditorsInterface; use Tobento\App\Block\ResourceResolverInterface; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots $app->boot(\Tobento\App\Block\Boot\Block::class); $app->booting(); // Implemented interfaces: $blockRepository = $app->get(BlockRepositoryInterface::class); $editors = $app->get(EditorsInterface::class); $resourceResolver = $app->get(ResourceResolverInterface::class); // Run the app $app->run();
Block Config
The configuration for the block is located in the app/config/block.php
file at the default App Skeleton config location where you can configure the block editors for your application.
Available Editors
Default Editor
This editor is the default implementation.
Configure Editor
In the Block Config you may configure the existing default
editor or creating new editors using the EditorFactory::class
.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'hero' => Editable\Hero::class, 'text' => Editable\Text::class, ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, 'text' => Factory\Text::class, ]); return $factory->createEditor(name: 'default'); }, ],
EditorFactory Methods
You may use the following methods to configure your editor to fit your requirements.
use Tobento\App\Block\BlockFactoryInterface; use Tobento\App\Block\BlockRepositoryInterface; use Tobento\App\Block\Editable; use Tobento\App\Block\Factory; use Tobento\Service\Language\LanguagesInterface; // Set editable blocks returning a new instance: $factory = $factory->withEditableBlocks([ 'text' => Editable\Text::class, ]); // Set block factories returning a new instance: $factory = $factory->withBlockFactories([ 'text' => Factory\Text::class, ]); // Set block factory returning a new instance: $factory = $factory->withBlockFactory($blockFactory); // BlockFactoryInterface // You may set another block repository returning a new instance: $factory = $factory->withBlockRepository($blockRepository); // BlockRepositoryInterface // You may set the available editor languages returning a new instance: $factory = $factory->withLanguages($languages); // LanguagesInterface // You may set another view namespace. Just make sure block views within that namespace exist. $blockFactory = $factory->blockFactory()->withViewNamespace('mail'); $factory = $factory->withBlockFactory($blockFactory);
You may check out the App Language to learn more about languages.
Render Editor
use Tobento\App\Block\EditorsInterface; use Tobento\Service\Responser\ResponserInterface; $app->route('GET', 'example/editor', function(EditorsInterface $editors, ResponserInterface $responser) { $editor = $editors->get('default'); // You may fetch existing blocks: $blocks = $editor->getBlockRepository()->findAll(where: [ 'id' => ['in' => [1,2]] ])->all(); return $responser->render( view: 'example/editor', data: [ 'editor' => $editor, 'blocks' => $blocks, ], ); });
In your view file, use the editor render
method to render the editor with its blocks using views located in the views/block
directory.
echo $editor->render( id: 'unique', blocks: $blocks, options: [ // you may set a block status: 'status' => 'active', // pending is default // you may set a resource id and group: 'resource_id' => 'articles:2', 'resource_group' => 'main', // you may set a position 'position' => 'header', // you may store blocks to a HTML input field 'storeBlocksToInput' => 'blocks', ], ); <input type="hidden" name="blocks">
You may check out the Crud Editor Field or Block Views Editor Middleware section which provides two ways to integrate editors.
Saving Editor
Blocks will be stored using the BlockRepository::class
with a pending
status if not set otherwise. Its up to you changing the status other than pending
.
Mail Editor
This editor will render blocks using views located in the views/block/mail
directory.
Configure Mail Editor
In the Block Config you may configure the existing mail
editor or creating new editors using the MailEditorFactory::class
which extends the EditorFactory::class
. So check out the Configure Editor for its available methods.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; use Tobento\App\Block\Mail\MailEditorFactory; 'editors' => [ 'mail' => static function (MailEditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'hero' => Editable\Hero::class, 'text' => Editable\Text::class, ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, 'text' => Factory\Text::class, ]); return $factory->createEditor(name: 'mail'); }, ],
Render Mail Editor
Check out the Render Editor section to learn more about it.
Crud Editor Field
You may use the BlockEditor::class
field to easily integrate a block editor when using the App CRUD.
use Tobento\App\Block\Crud\Field\BlockEditor; BlockEditor::new('blocks')->editor(name: 'default');
Workflow
Blocks will be stored using the block repository with a pending
status when a CRUD resource has not been saved. Once the CRUD resource is saved, the status will be changed to active
. When a CRUD resource gets deleted, the status will be changed back to pending
. In addition, blocks will be stored in JSON format in the specified CRUD BlockEditor field.
To clean pending
blocks consider using the Purge Blocks Command.
Render Blocks
There are many ways how to render the stored blocks. One way is to use the editors block factory to render the created blocks stored in your CRUD resource.
use Tobento\App\Block\EditorsInterface; $editors = $app->get(EditorsInterface::class); $editor = $editors->get('default'); $html = ''; foreach($storedBlocks as $block) { $block['editable'] = false; // set blocks as uneditable. $block['locale'] = 'de'; // you may change its locale. $entity = $editor->getBlockRepository()->createEntity($block); $html .= $editor->getBlockFactory()->createBlockFromEntity($entity)->render(); } echo $html;
Block Views Editor Middleware
The block views editor middleware integrates block editors based on the defined block views and the specified or resolved resource.
Set up
use Tobento\App\AppFactory; use Tobento\App\Block\Middleware\BlockViewsEditor; use Tobento\Service\Responser\ResponserInterface; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots $app->boot(\Tobento\App\Block\Boot\Block::class); $app->booting(); // Routes: $app->route('GET', 'about', function(ResponserInterface $responser) { return $responser->render( view: 'about', data: [], ); })->middleware([ BlockViewsEditor::class, 'editorName' => 'default', 'editable' => true, // you may set a resource id and/or group // modifying the resource resolved from the resource resolver: 'resourceId' => 'about', 'resourceGroup' => 'main', ]); // Run the app $app->run();
You may configure the resource resolver in the Block Config file:
'interfaces' => [ \Tobento\App\Block\ResourceResolverInterface::class => \Tobento\App\Block\ResourceResolver\Slugs::class, ],
View
Views starting with blocks.resource
are specific to its defined resource , meaning blocks will only be rendered on the matching resource. Views such as blocks.header
and blocks.footer
will always render its blocks within the same resourceGroup
but indepedently of the resourceId
. You can define as many views you like.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>About</title> <?= $view->render('inc/head') ?> <?= $view->assets()->render() ?> </head> <body> <header class="page-header"> <?= $view->render('blocks.header') ?> </header> <main class="page-main"> <?= $view->render('blocks.resource') ?> <p>Some content</p> <?= $view->render('blocks.resource.footer') ?> </main> <footer class="page-footer"> <?= $view->render('blocks.footer') ?> </footer> </body> </html>
Deleting Blocks
If your resource is a App CRUD being resolved by the slugs, you may use BlockResourceEditor::class
field, which will delete blocks while deleting the field.
use Tobento\App\Block\Crud\Field\BlockResourceEditor; use Tobento\App\Crud\Entity\EntityInterface; BlockResourceEditor::new() ->editor('default') // Set the supported block positions: ->blockPositions('resource.header', 'resource') // You may customize the resource id: ->resourceId(fn (EntityInterface $entity): string => sprintf('articles:%s', $entity->id())) // By default, the CRUD resource name and the entity id is used e.g. 'articles:45' // matching the slugs resource resolver pattern. // You may set a resource group: ->resourceGroup(name: 'main') // You may disable blocks being editable as they are editable by the middleware: ->editable(false) // default true // You may enable to store blocks on its field // (not recommended if using the middleware to edit blocks as data are not in sync): ->storable(true); // default false
Otherwise, you will need to implement your own logic using the block repository.
Available Blocks
Downloads Block
This block lets you add files to be displayed for download or be viewed in browser.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'downloads' => Editable\Downloads::class, // Or: 'downloads' => new Editable\Downloads( // you may customize the picture definitions: pictureDefinitions: ['block-downloads'], // default // you may configure the allowed file extensions: allowedFileExtensions: ['jpg', 'png', 'webp', 'pdf'], // default // you may set the max number of files allowed: maxNumberOfFiles: 50, // default ), ]); $factory->addBlockFactories([ 'downloads' => Factory\Downloads::class, // you may generate images immediately: 'downloads' => [Factory\Downloads::class, 'generateImagesInBackground' => false], ]); return $factory->createEditor(name: 'default'); }, ],
Requirements
Make sure you have the downloads
storage added on the supportedStorages
parameter on each of the following features in the config/media.php
file:
'features' => [ new Feature\File( supportedStorages: ['images', 'uploads', 'downloads'], ), new Feature\FileDownload( supportedStorages: ['downloads'], ), new Feature\FileDisplay( supportedStorages: ['downloads'], ), ],
You may check out the App Media for more information.
Hero Block
This block creates an editable text block using the Js Editor and lets you add an image to be displayed.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'hero' => Editable\Hero::class, // you may customize the picture definitions: 'hero' => new Editable\Hero( pictureDefinitions: ['block-hero'], // default ), ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, // you may generate images immediately: 'hero' => [Factory\Hero::class, 'generateImagesInBackground' => false], ]); return $factory->createEditor(name: 'default'); }, ],
Image Block
This block lets you add an image to be displayed.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'image' => Editable\Image::class, // you may customize the picture definitions: 'image' => new Editable\Image( pictureDefinitions: ['block-image'], // default ), ]); $factory->addBlockFactories([ 'image' => Factory\Image::class, // you may generate images immediately: 'image' => [Factory\Image::class, 'generateImagesInBackground' => false], ]); return $factory->createEditor(name: 'default'); }, ],
Image Gallery Block
This block lets you add multiple images to be displayed as a gallery. Clicking on an image opens up a modal with bigger sized images.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'image-gallery' => Editable\ImageGallery::class, // Or: 'image-gallery' => new Editable\ImageGallery( // you may customize the picture definitions: pictureDefinitions: [ 'block-image-gallery', // default 'block-image-gallery-large', // you may add which is used for large images. ], // you may set the max number of images allowed: maxNumberOfImages: 50, // default ), ]); $factory->addBlockFactories([ 'image-gallery' => Factory\ImageGallery::class, // you may generate images immediately: 'image-gallery' => [Factory\ImageGallery::class, 'generateImagesInBackground' => false], ]); return $factory->createEditor(name: 'default'); }, ],
Persons Block
This block lets you add persons to be displayed. For instance, you add a team section.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'persons' => Editable\Persons::class, // you may customize the picture definitions: 'persons' => new Editable\Persons( pictureDefinitions: ['block-persons'], // default ), ]); $factory->addBlockFactories([ 'persons' => Factory\Persons::class, // you may generate images immediately: 'persons' => [Factory\Persons::class, 'generateImagesInBackground' => false], ]); return $factory->createEditor(name: 'default'); }, ],
Text Block
This block creates an editable text block using the Js Editor.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'text' => Editable\Text::class, ]); $factory->addBlockFactories([ 'text' => Factory\Text::class, ]); return $factory->createEditor(name: 'default'); }, ],
Block Options
You can configure the block options in the app/config/block.php
file in the interfaces section:
use Tobento\App\Block\Editable\Option as EditableOption; use Tobento\App\Block\Editable\Option\Options as EditableOptions; use Tobento\App\Block\Editable\Option\OptionsInterface as EditableOptionsInterface; 'interfaces' => [ \Tobento\App\Block\Block\Option\OptionsFactoryInterface::class => \Tobento\App\Block\Block\Option\OptionsFactory::class, EditableOptionsInterface::class => static function(): EditableOptionsInterface { return new EditableOptions([ 'padding' => new EditableOption\Padding(), 'margin' => new EditableOption\Margin(), 'color' => new EditableOption\Color(), ]); }, ],
You may customize editable block options for each block separately:
use Tobento\App\Block\Editable; use Tobento\App\Block\Editable\Option as EditableOption; use Tobento\App\Block\Editable\Option\Options as EditableOptions; use Tobento\App\Block\Editable\Option\OptionsInterface as EditableOptionsInterface; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory, EditableOptionsInterface $editableOptions): EditorInterface { $factory->addEditableBlocks([ 'hero' => new Editable\Hero( options: $editableOptions->withOption( name: 'layout', option: new EditableOption\Layout(['foo' => 'Foo']) ), ), ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, ]); return $factory->createEditor(name: 'default'); }, ],
Available Methods:
// Adds an option returning a new instance: $editableOptions = $editableOptions->withOption( name: 'layout', option: new EditableOption\Layout(['foo' => 'Foo']) ); // Returns a new instance ONLY with the specified options: $editableOptions = $editableOptions->only('padding', 'margin'); // Returns a new instance EXCEPT with the specified options: $editableOptions = $editableOptions->except('padding', 'margin'); // Returns a new instance with the options orderd by the specified names: $editableOptions = $editableOptions->reorder('padding', 'margin');
Available Block Options
Classes Option
The classes option lets you select multiple CSS classes to be assigned on the block.
use Tobento\App\Block\Editable\Option as EditableOption; use Tobento\App\Block\Editable\Option\Options as EditableOptions; use Tobento\App\Block\Editable\Option\OptionsInterface as EditableOptionsInterface; 'interfaces' => [ EditableOptionsInterface::class => static function(): EditableOptionsInterface { return new EditableOptions([ 'classes' => new EditableOption\Classes( // You may set custom classes, otherwise default are used: classes: ['classname' => 'A title'], // You may disable searching classes if you have only a few class: searchableClasses: false, // true is default // You may change the group name: groupName: 'Classes', // default ), ]); }, ],
Color Option
The color option lets you select a color for the background and text.
use Tobento\App\Block\Editable\Option as EditableOption; use Tobento\App\Block\Editable\Option\Options as EditableOptions; use Tobento\App\Block\Editable\Option\OptionsInterface as EditableOptionsInterface; 'interfaces' => [ EditableOptionsInterface::class => static function(): EditableOptionsInterface { return new EditableOptions([ 'color' => new EditableOption\Color( supportedColors: ['text', 'background'] // default ), ]); }, ],
Layout Option
The layout option lets you define multiple layouts for the block if supported.
use Tobento\App\Block\Editable; use Tobento\App\Block\Editable\Option as EditableOption; use Tobento\App\Block\Editable\Option\Options as EditableOptions; use Tobento\App\Block\Editable\Option\OptionsInterface as EditableOptionsInterface; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'default' => static function (EditorFactory $factory, EditableOptionsInterface $editableOptions): EditorInterface { $factory->addEditableBlocks([ 'hero' => new Editable\Hero( options: $editableOptions->withOption( name: 'layout', option: new EditableOption\Layout(['fit' => 'Fit Image']) ), ), ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, ]); return $factory->createEditor(name: 'default'); }, ],
Make sure, you have created the corresponding view file like views/block/hero-fit
and views/block/hero-editor-fit
, otherwise the default view file is used.
Margin And Padding Option
The margin and padding option lets you select a margin and/or padding size.
use Tobento\App\Block\Editable\Option as EditableOption; use Tobento\App\Block\Editable\Option\Options as EditableOptions; use Tobento\App\Block\Editable\Option\OptionsInterface as EditableOptionsInterface; 'interfaces' => [ EditableOptionsInterface::class => static function(): EditableOptionsInterface { return new EditableOptions([ 'margin' => new EditableOption\Margin( supportedMargin: ['top', 'bottom', 'left', 'right'], // default ), 'padding' => new EditableOption\Padding( supportedPadding: ['top', 'bottom', 'left', 'right'], // default ), ]); }, ],
Deleting Generated Pictures
Blocks such as the Image Block generate pictures using the Media Picture Feature.
To clear generated pictures, once a block is updated or deleted, you will need to define an event listener in the app/config/event.php
file:
'listeners' => [ \Tobento\App\Crud\Event\FileSourceDeleted::class => [ \Tobento\App\Crud\Listener\DeletesGeneratedPictures::class, ], ],
Console
Purge Blocks Command
Use the following command to purge pending
blocks:
php ap blocks:purge
If you would like to automate this process, consider installing the App Schedule bundle and using a command task:
use Tobento\Service\Schedule\Task; use Butschster\CronExpression\Generator; $schedule->task( (new Task\CommandTask( command: 'php ap blocks:purge', )) // schedule task: ->cron(Generator::create()->daily()) );
Learn More
Creating Custom Editor
Option 1 with view namespace only
In the Block Config file just add a custom viewNamespace
.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'custom' => static function (EditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'hero' => Editable\Hero::class, 'text' => Editable\Text::class, ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, 'text' => Factory\Text::class, ]); // add custom namespace $blockFactory = $factory->blockFactory()->withViewNamespace('custom'); $factory = $factory->withBlockFactory($blockFactory); return $factory->createEditor(name: 'custom'); }, ],
Finally, add the view files in the viewNamespace
defined which you want to customize skipping others you do not want to customize.
views/block/custom/
hero.php
hero-editor.php
...
Option 2 with custom editor factory
By creating a custom editor factory, you will be able to add blocks using the factory.
First, create the editor factory by extending the EditorFactory::class
:
use Tobento\App\Block\BlockFactoryInterface; use Tobento\App\Block\Editor\BlockFactory; use Tobento\App\Block\Editor\EditorFactory; class CustomEditorFactory extends EditorFactory { /** * Returns the created block factory. * * @return BlockFactoryInterface */ protected function createBlockFactory(): BlockFactoryInterface { return new BlockFactory(container: $this->container, viewNamespace: 'custom'); } }
Next, add the view files in the viewNamespace
defined which you want to customize skipping others you do not want to customize.
views/block/custom/
hero.php
hero-editor.php
...
Finally, configure your editor in the Block Config file.
use Tobento\App\Block\Editable; use Tobento\App\Block\EditorInterface; use Tobento\App\Block\Factory; use Tobento\App\Block\Editor\EditorFactory; 'editors' => [ 'custom' => static function (CustomEditorFactory $factory): EditorInterface { $factory->addEditableBlocks([ 'hero' => Editable\Hero::class, 'text' => Editable\Text::class, ]); $factory->addBlockFactories([ 'hero' => Factory\Hero::class, 'text' => Factory\Text::class, ]); return $factory->createEditor(name: 'custom'); }, ],
Adding Blocks Using Editor Factories
It may be useful to add blocks using the editor factories from within the app if you have different components such as a Shop component providing specific shop blocks.
use Tobento\App\Block\Editor\EditorFactory; $app->on( EditorFactory::class, static function(EditorFactory $factory): void { $factory->addEditableBlocks([ 'products' => ProductListEditable::class, ]); $factory->addBlockFactories([ 'products' => ProductListFactory::class, ]); } );