tobento/app-block

1.0.0 2025-07-15 12:29 UTC

This package is auto-updated.

Last update: 2025-07-15 12:33:44 UTC


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

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,
        ]);
    }
);

Credits