PHP code example of tobento / app-crud

1. Go to this page and download the library: Download tobento/app-crud library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

tobento / app-crud example snippets


use Tobento\App\AppFactory;

$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').'vendor', 'vendor')
    ->dir($app->dir('app').'views', 'views', group: 'views')
    ->dir($app->dir('app').'trans', 'trans', group: 'trans')
    ->dir($app->dir('root').'build/public', 'public');

// Adding boots:

// you might boot error handlers:
$app->boot(\Tobento\App\Boot\ErrorHandling::class);

$app->boot(\Tobento\App\Crud\Boot\Crud::class);

// you might boot:
$app->boot(\Tobento\App\View\Boot\Breadcrumb::class);
$app->boot(\Tobento\App\View\Boot\Messages::class);

// Run the app:
$app->run();

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field\FieldInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Action;
use Tobento\App\Crud\Filter\FiltersInterface;
use Tobento\App\Crud\Filter\FilterInterface;
use Tobento\App\Crud\Filter;
use Tobento\Service\Repository\RepositoryInterface;

class ProductsController extends AbstractCrudController
{
    /**
     * Must be unique, lowercase and only of [a-z-] characters.
     */
    public const RESOURCE_NAME = 'products';
    
    /**
     * Create a new ProductController.
     *
     * @param RepositoryInterface $repository
     */
    public function __construct(
        ProductRepository $repository
    ) {
        $this->repository = $repository;
    }
    
    /**
     * Returns the configured fields.
     *
     * @param ActionInterface $action
     * @return iterable<FieldInterface>|FieldsInterface
     */
    protected function configureFields(ActionInterface $action): iterable|FieldsInterface
    {
        return [
            Field\PrimaryId::new('id'),
            Field\Text::new('sku'),
            //...
        ];
    }
    
    /**
     * Returns the configured actions.
     *
     * @return iterable<ActionInterface>|ActionsInterface
     */
    protected function configureActions(): iterable|ActionsInterface
    {
        return [
            Action\Index::new(title: 'Products'),
            //...
        ];
    }
    
    /**
     * Returns the configured filters.
     *
     * @param ActionInterface $action
     * @return iterable<FilterInterface>|FiltersInterface
     */
    protected function configureFilters(ActionInterface $action): iterable|FiltersInterface
    {
        return [
            Filter\Columns::new()->open(false),
            //...
        ];
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Entity\Entity;
use Tobento\App\Crud\Entity\EntityInterface;
use Tobento\Service\Support\Arrayable;

class ProductsController extends AbstractCrudController
{
    /**
     * Create entity from object.
     *
     * @param object $object
     * @return EntityInterface
     */
    public function createEntityFromObject(object $object): EntityInterface
    {
        // Default mapping:
        if ($object instanceof Arrayable) {
            return new Entity(
                attributes: $object->toArray(),
                idAttributeName: $this->entityIdName(),
            );
        }
        
        if (
            method_exists($object, 'toArray')
            && is_array($array = $object->toArray())
        ) {
            return new Entity(
                attributes: $array,
                idAttributeName: $this->entityIdName(),
            );
        }
        
        return new Entity(
            attributes: (array)$object,
            idAttributeName: $this->entityIdName(),
        );
    }
}

use Tobento\App\Crud\AbstractCrudController;

class ProductsController extends AbstractCrudController
{
    /**
     * Returns the entity id name.
     *
     * @return string
     */
    protected function entityIdName(): string
    {
        return 'id';
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Filter\FiltersInterface;

class ProductsController extends AbstractCrudController
{
    /**
     * Find entities.
     *
     * @param FiltersInterface $filters
     * @return iterable The found entities.
     */
    public function findEntities(FiltersInterface $filters): iterable
    {
        return $this->repository()->findAll(
            where: $filters->getWhereParameters(),
            orderBy: $filters->getOrderByParameters(),
            limit: $filters->getLimitParameter(),
        );
    }
}

use Tobento\App\Crud\AbstractCrudController;

class ProductsController extends AbstractCrudController
{
    /**
     * Store entity.
     *
     * @param array $attributes
     * @return object The created entity
     */
    public function storeEntity(array $attributes): object
    {
        return $this->repository()->create(
            attributes: $attributes,
        );
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Entity\EntityInterface;

class ProductsController extends AbstractCrudController
{
    /**
     * Update entity.
     *
     * @param int|string $id
     * @param array $attributes
     * @param EntityInterface $entity
     * @return object The updated entity
     */
    public function updateEntity(int|string $id, array $attributes, EntityInterface $entity): object
    {
        return $this->repository()->updateById(
            id: $id,
            attributes: $attributes,
        );
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Entity\EntityInterface;

class ProductsController extends AbstractCrudController
{
    /**
     * Delete entity.
     *
     * @param int|string $id
     * @param EntityInterface $entity
     * @return void
     */
    public function deleteEntity(int|string $id, EntityInterface $entity): void
    {
        $this->repository()->deleteById(id: $id);
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field\FieldInterface;
use Tobento\App\Crud\Field;

class ProductsController extends AbstractCrudController
{
    /**
     * Returns the configured fields.
     *
     * @param ActionInterface $action
     * @return iterable<FieldInterface>|FieldsInterface
     */
    protected function configureFields(ActionInterface $action): iterable|FieldsInterface
    {
        return [
            Field\PrimaryId::new('id'),
            Field\Text::new(name: 'sku'),
            //...
        ];
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Action;

class ProductsController extends AbstractCrudController
{
    /**
     * Returns the configured actions.
     *
     * @return iterable<ActionInterface>|ActionsInterface
     */
    protected function configureActions(): iterable|ActionsInterface
    {
        return [
            Action\Index::new(title: 'Products'),
            Action\Create::new(title: 'New product'),
            Action\Store::new(),
            Action\Edit::new(title: 'Edit product'),
            Action\Update::new(),
            Action\Copy::new(title: 'Copy product'),
            Action\Show::new(),
            Action\Delete::new(),
            Action\BulkDelete::new(),
            Action\BulkEdit::new(),
        ];
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Filter\FiltersInterface;
use Tobento\App\Crud\Filter\FilterInterface;
use Tobento\App\Crud\Filter;

class ProductsController extends AbstractCrudController
{
    /**
     * Returns the configured filters.
     *
     * @param ActionInterface $action
     * @return iterable<FilterInterface>|FiltersInterface
     */
    protected function configureFilters(ActionInterface $action): iterable|FiltersInterface
    {
        return [
            Filter\Columns::new()->open(false),
            Filter\FieldsSortOrder::new(),
            ...Filter\Fields::new()
               ->group('field')
               ->fields($action->fields())
               ->toFilters(),
            Filter\PaginationItemsPerPage::new()->open(false),
            
            // must be added last!
            Filter\Pagination::new(),
        ];
    }
}

use Tobento\App\Boot;
use Tobento\App\Crud\Boot\Crud;

class RoutesBoot extends Boot
{
    public const BOOT = [
        // you may ensure the crud boot:
        Crud::class,
    ];
    
    public function boot(Crud $crud)
    {
        $crud->routeController(App\ProductsController::class);
        
        // or:
        $crud->routeController(
            controller: App\ProductsController::class,
            // you may set specific actions only:
            only: ['index', 'show'],
            // or you may exclude specific actions:
            except: ['bulk'],
            // you may set middlewares for all routes:
            middleware: [
                SomeMiddleware::class,
            ],
            // you may localize the routes:
            localized: true,
        );
    }
}

use Tobento\Service\Routing\RouterInterface;

// After adding boots
$app->booting();

$router = $this->app->get(RouterInterface::class);

$name = App\ProductsController::RESOURCE_NAME;

$router->resource($name, App\ProductsController::class);

// needed if you have configured bulk actions:
$router->post($name.'/bulk/{name}', [App\ProductsController::class, 'bulk'])
    ->name($name.'.bulk');
    
// Run the app:
$app->run();

use Tobento\App\Boot;
use Tobento\App\Crud\Boot\Crud;

class RoutesBoot extends Boot
{
    public function boot(Crud $crud)
    {
        $crud->routeController(
            controller: App\ProductsController::class,
            middleware: [
                [
                    \Tobento\App\User\Middleware\VerifyRoutePermission::class,
                    'permissions' => [
                        'products.index' => 'products',
                        'products.show' => 'products',
                        'products.create' => 'products.create',
                        'products.store' => 'products.create',
                        'products.copy' => 'products.create',
                        'products.edit' => 'products.edit',
                        'products.update' => 'products.edit',
                        'products.delete' => 'products.delete',
                        'products.bulk' => 'products.edit|products.delete',
                    ],
                ]
            ],
        );
    }
}

use Tobento\App\Crud\Field;

Field\Checkboxes::new(
    name: 'colors',
    // you may set a label, otherwise name is used:
    label: 'Colors',
);

Field\Checkboxes::new('colors')
    // specify the options using an array:
    ->options(['blue' => 'Blue', 'red' => 'Red'])
    
    // or using a closure (parameters are resolved by autowiring):
    ->options(fn(ColorsRepository $repo): array => $repo->findColors());

Field\Checkboxes::new('colors')
    ->emptyOption(value: '_none');

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\FieldInterface;

Field\Checkboxes::new('colors')
    ->selected(value: ['blue', 'red'], action: 'create')
    
    // or using a closure (additional parameters are resolved by autowiring):
    ->selected(
        value: function (ActionInterface $action, FieldInterface $field): null|array {
            return ['blue', 'red'];
            //return null; // if none is selected
        },
        action: 'edit',
    )

use Tobento\App\Crud\Field;

Field\Checkboxes::new('colors')->attributes(['class' => 'name']);

Field\Checkboxes::new('colors')
    ->validate('

use Tobento\App\Crud\Field;

Field\File::new(
    name: 'file',
    // you may set a label, otherwise name is used:
    label: 'File',
);

use Tobento\App\Crud\Field;

Field\File::new(name: 'file')
    ->fields(
        Field\Text::new('alt', 'Alternative Text')->translatable(),
        Field\Radios::new('buyable', 'Buyable')->displayInline()->options(['no', 'yes']),
    );

use Tobento\App\Crud\Field;

Field\File::new(name: 'file')
    ->translatable();

use Tobento\App\Crud\Field;

Field\File::new(name: 'file')
    ->fileSource(function(Field\FileSource $fs): void {
        $fs->storage('uploads')
           ->allowedExtensions('jpg', 'png')
           ->imageEditor(template: 'default');
    });

use Tobento\App\Crud\Field;

Field\File::new(name: 'file')
    ->fields(
        Field\Text::new('name', 'Filename')->translatable(),
    )
    ->storeFilenameTo(field: 'name');
    
    // with using a filename modifier (callable):
    ->storeFilenameTo(field: 'name', modify: static function(string $filename): string {
        return $filename;
    });

use Tobento\App\Crud\Field;

Field\Files::new(
    name: 'files',
    // you may set a label, otherwise name is used:
    label: 'Files',
);

use Tobento\App\Crud\Field;

Field\Files::new(name: 'files')
    ->fields(
        Field\Text::new('alt', 'Alternative Text')->translatable(),
        Field\Radios::new('buyable', 'Buyable')->displayInline()->options(['no', 'yes']),
    );

use Tobento\App\Crud\Field;

Field\Files::new(name: 'files')
    ->file(function(Field\File $file): void {
        $file->translatable();
        $file->fileSource(function(Field\FileSource $fs): void {
            $fs->allowedExtensions('png');
        });
    });

use Tobento\App\Crud\Field;

Field\Files::new(name: 'files')
    // min only;
    ->numberOfFiles(min: 1)
    // or max only:
    ->numberOfFiles(max: 10)
    // or min and max:
    ->numberOfFiles(min: 1, max: 10);

use Tobento\App\Crud\Field;

Field\FileSource::new(
    name: 'file',
    // you may set a label, otherwise name is used:
    label: 'File',
);

Field\FileSource::new('image')
    ->storage(name: 'custom-uploads');

Field\FileSource::new('image')
    ->folder(path: 'shop/products')
    
    // or using a callable:
    ->folder(static function(): string {
        return sprintf('product/%s/%s/', date('Y'), date('m'));
    });

Field\FileSource::new('image')
    ->allowedExtensions('jpg', 'png')
    
    // you may set max file size in KB:
    ->maxFileSizeInKb(1000); // or null unlimited (default)

use Tobento\App\Media\Upload\ValidatorInterface;
use Tobento\App\Media\Upload\Validator;

Field\FileSource::new('image')
    ->validator(static function(): ValidatorInterface {
        return new Validator(
            allowedExtensions: ['jpg'],
            strictFilenameCharacters: true,
            maxFilenameLength: 200,
            maxFileSizeInKb: 2000,
        );
        
        // or create your custom validator implementing the ValidatorInterface!
    });

use Tobento\App\Media\FileStorage\FileWriter;
use Tobento\App\Media\FileStorage\FileWriterInterface;
use Tobento\App\Media\FileStorage\Writer;
use Tobento\App\Media\Image\ImageProcessor;
use Tobento\Service\FileStorage\StorageInterface;

Field\FileSource::new('image')
    ->fileWriter(static function(StorageInterface $storage): FileWriterInterface {
        return new FileWriter(
            storage: $storage,
            filenames: FileWriter::ALNUM, // RENAME, ALNUM, KEEP
            duplicates: FileWriter::RENAME, // RENAME, OVERWRITE, DENY
            folders: FileWriter::ALNUM, // or KEEP
            folderDepthLimit: 5,
            writers: [
                new Writer\ImageWriter(
                    imageProcessor: new ImageProcessor(
                        actions: [
                            'orientate' => [],
                            'resize' => ['width' => 2000],
                        ],
                    ),
                ),
                new Writer\SvgSanitizerWriter(),
            ],
        );
    });

use Tobento\Service\Picture\DefinitionInterface;
use Tobento\Service\Picture\Definition\ArrayDefinition;

Field\FileSource::new('image')
    ->picture(definition: [
        'img' => [
            'src' => [120],
            'loading' => 'lazy',
        ],
        'sources' => [
            [
                'srcset' => [
                    '' => [120],
                ],
                'type' => 'image/webp',
            ],
        ],
    ])
    
    // or you may use a definition object implementing the DefinitionInterface:
    ->picture(definition: new ArrayDefinition('product-image', [
        'img' => [
            'src' => [120],
            'loading' => 'lazy',
        ],
        'sources' => [
            [
                'srcset' => [
                    '' => [120],
                ],
                'type' => 'image/webp',
            ],
        ],
    ]))
    
    // or you may disable it, showing just the path:
    ->picture(definition: null)
    
    // You may queue the picture generation:
    ->pictureQueue(true);

Field\FileSource::new('image')
    ->imageEditor(template: 'crud');

'listeners' => [
    \Tobento\App\Media\Event\ImageEdited::class => [
        [\Tobento\App\Crud\Listener\ClearGeneratedPicture::class, ['definition' => 'crud-file-src']],
        
        // you add more when using different definitions:
        [\Tobento\App\Crud\Listener\ClearGeneratedPicture::class, ['definition' => 'product-image']],
    ],
],

Field\FileSource::new('image')
    ->pictureEditor(template: 'default', definitions: ['product-main', 'product-list']);

Field\FileSource::new('image')
    ->displayMessages('error', 'success', 'info', 'notice');

use Tobento\App\Crud\Field;

Field\Html::new(
    name: 'title',
    // you may set a label, otherwise name is used:
    label: 'TITLE',
);

use Tobento\App\Crud\Field;

Field\Html::new(name: 'title')->content(html: '<p>Lorem</p>');

use Tobento\App\Crud\Field;
use Tobento\Service\View\ViewInterface;

Field\Html::new(name: 'title')->content(function (ViewInterface $view): string {
    return $view->render('about', []);
});

use Tobento\App\Crud\Field;

Field\Html::new(name: 'title')
    ->content(html: '<p>Lorem</p>')
    ->indexable(true) // default false
    ->showable(true); // default false

use Tobento\App\Crud\Field;

Field\Items::new('prices')
    // you may group the fields
    ->group('Prices') // set before defining fields!
    
    // define the fields per item:
    ->fields(
        Field\Text::new('price_net', 'Price Net')
            ->type('number')
            ->attributes(['step' => 'any'])
            ->validate('decimal'),
        Field\Text::new('price_gross', 'Price Gross')
            ->type('number')
            ->attributes(['step' => 'any'])
            ->validate('decimal'),
    )
    
    // you may display items as card layout:
    ->displayAsCard()
    
    // you may restrict items:
    ->validate('

use Tobento\App\Crud\Field;

Field\Options::new(
    name: 'categories',
    // you may set a label, otherwise name is used:
    label: 'Categories',
);

use Tobento\Service\Repository\RepositoryInterface;

Field\Options::new('categories')
    ->repository(CategoriesRepository::class) // class-string|RepositoryInterface
    
    // you may change the limit of the searchable options to be displayed:
    ->limit(15); // default is 25

    // you may change the column value to be stored:
    ->storeColumn('sku') // 'id' is default
    
    // you may change the search columns:
    ->searchColumns('title', 'sku'); // 'title' is default

use Tobento\App\Crud\Field;
use Tobento\Service\View\ViewInterface;

Field\Options::new('categories')
    ->toOption(function(object $item, ViewInterface $view, Field\Options $options): Field\Option {        
        return new Field\Option(
            value: (string)$item->get('id'),
            text: (string)$item->get('title'),
        );
    });

use Tobento\App\Crud\Field;
use Tobento\Service\View\ViewInterface;

Field\Options::new('categories')
    ->toOption(function(object $item, ViewInterface $view, Field\Options $options): Field\Option {
        return (new Field\Option(value: (string)$item->get('id')))
            ->text((string)$item->get('title'))
            ->text((string)$item->get('sku'))
            ->html('html'); // must be escaped!
    });

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\FieldInterface;

Field\Options::new('categories')
    ->selected(value: ['2', '5'], action: 'create')
    
    // or using a closure (additional parameters are resolved by autowiring):
    ->selected(
        value: function (ActionInterface $action, FieldInterface $field): null|array {
            return ['2', '5'];
            //return null; // if none is selected
        },
        action: 'edit',
    )

Field\Options::new('categories')
    ->placeholder(text: 'Search categories');

Field\Options::new('categories')
    ->emptyOption(value: '_none');

Field\Options::new('categories')
    ->validate('

use Tobento\App\Crud\Field;

Field\PrimaryId::new(name: 'id', label: 'ID');

use Tobento\App\Crud\Field;

Field\Radios::new(
    name: 'colors',
    // you may set a label, otherwise name is used:
    label: 'Colors',
);

Field\Radios::new('colors')
    // specify the options using an array:
    ->options(['blue' => 'Blue', 'red' => 'Red'])
    
    // or using a closure (parameters are resolved by autowiring):
    ->options(fn(ColorsRepository $repo): array => $repo->findColors());

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\FieldInterface;

Field\Radios::new('colors')
    ->selected(value: 'blue', action: 'create')
    
    // or using a closure (additional parameters are resolved by autowiring):
    ->selected(
        value: function (ActionInterface $action, FieldInterface $field): null|string {
            return 'blue';
            //return null; // if none is selected
        },
        action: 'edit',
    )

use Tobento\App\Crud\Field;

Field\Radios::new('colors')->attributes(['class' => 'name']);

Field\Radios::new('colors')
    ->displayInline();

Field\Radios::new('colors')
    ->validate('

use Tobento\App\Crud\Field;

Field\Select::new(
    name: 'colors',
    // you may set a label, otherwise name is used:
    label: 'Colors',
);

Field\Select::new('colors')
    // specify the options using an array:
    ->options(['blue' => 'Blue', 'red' => 'Red'])
    
    // or using a closure (parameters are resolved by autowiring):
    ->options(fn(ColorsRepository $repo): array => $repo->findColors());

Field\Select::new('roles')
    ->options([
        'Frontend' => [
            'guest' => 'Guest',
            'registered' => 'Registered',
        ],
        'Backend' => [
            'editor' => 'Editor',
            'administrator' => 'Aministrator',
        ],
    ]);

Field\Select::new('colors')
    // specify the options using an array:
    ->options(['blue' => 'Blue', 'red' => 'Red'])
    ->emptyOption(value: 'none', label: '---');

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\FieldInterface;

Field\Select::new('colors')
    ->selected(value: 'value', action: 'create')
    
    // or using a closure (additional parameters are resolved by autowiring):
    ->selected(
        value: function (ActionInterface $action, FieldInterface $field): null|string|array {
            return ['blue', 'red']; // if multiple selection
            //return 'blue'; // if single selection
            //return null; // if none is selected
        },
        action: 'edit',
    )

use Tobento\App\Crud\Field;

Field\Select::new('colors')->attributes(['multiple', 'size' => '10']);

Field\Select::new('colors')
    ->validate('ction:
Field\Select::new('colors')
    ->attributes(['multiple', 'size' => '10'])
    ->validate('

use Tobento\App\Crud\Field;

Field\Slug::new(
    name: 'slug',
    // you may set a label, otherwise name is used:
    label: 'SLUG',
);

use Tobento\App\Crud\Field;

Field\Slug::new('slug')->fromField('title');

use Tobento\App\Crud\Field;
use Tobento\Service\Slugifier\SlugifierInterface;
use Tobento\Service\Slugifier\SlugifiersInterface;

// using slugifier name:
Field\Slug::new('slug')->slugifier('crud'); 
// 'crud' is set as default but fallsback to default as not defined in slugging config

// using object:
Field\Slug::new('slug')->slugifier(new Slugifier());

// using closure:
Field\Slug::new('slug')
    ->slugifier(function (SlugifiersInterface $slugifiers): SlugifierInterface {
        return $slugifiers->get('custom');
    });

use Tobento\App\Crud\Field;

Field\Slug::new('slug')->uniqueSlugs(false);

use Tobento\App\Crud\Field;

Field\Slug::new(name: 'title')->attributes(['data-foo' => 'value']);

use Tobento\App\Crud\Field;

Field\Slug::new('slug')
    ->fromField('title')
    ->translatable()
    ->readonly(true, action: 'edit|update');

use Tobento\App\Crud\Field;

Field\Text::new(
    name: 'title',
    // you may set a label, otherwise name is used:
    label: 'TITLE',
);

use Tobento\App\Crud\Field;

Field\Text::new(name: 'email')->type('email');

use Tobento\App\Crud\Field;

Field\Text::new(name: 'title')->value('Lorem');

// you may pass an array of values if your field is translatable:
Field\Text::new(name: 'title')
    ->translatable()
    ->value(['en' => 'Lorem', 'de' => 'Lorem ipsum']);

use Tobento\App\Crud\Field;

Field\Text::new(name: 'title')->defaultValue('Lorem');

// you may pass an array of values if your field is translatable:
Field\Text::new(name: 'title')
    ->translatable()
    ->defaultValue(['en' => 'Lorem', 'de' => 'Lorem ipsum']);

use Tobento\App\Crud\Field;

Field\Text::new(name: 'title')->attributes(['data-foo' => 'value']);

use Tobento\App\Crud\Field;

Field\Textarea::new(
    name: 'title',
    // you may set a label, otherwise name is used:
    label: 'TITLE',
);

use Tobento\App\Crud\Field;

Field\Textarea::new(name: 'title')->attributes(['rows' => '5']);

use Tobento\App\Crud\Field;

Field\TextEditor::new(
    name: 'desc',
    // you may set a label, otherwise name is used:
    label: 'Description',
);

use Tobento\App\Crud\Field;

Field\TextEditor::new(name: 'desc')
    ->editorConfig([
        'toolbar' => [
            'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
            'bold', 'italic', 'underline', 'strike',
            'ol', 'ul', 'quote', 'pre', 'code',
            'undo', 'redo', 'sourcecode', 'clear',
            'link', 'tables', 'style.fonts', 'style.text.colors'
        ]
    ]);

use Tobento\Service\Repository\Storage\Column\Text;
use Tobento\Service\Repository\Storage\Column\ColumnsInterface;
use Tobento\Service\Repository\Storage\StorageRepository;
use function Tobento\App\HtmlSanitizer\sanitizeHtml;

class ExampleRepository extends StorageRepository
{
    protected function configureColumns(): iterable|ColumnsInterface
    {
        return [
            // ...
            Column\Text::new('desc', type: 'text')
                // as there might be data stored before, we clean the html on reading:
                ->read(fn (string $value): string => sanitizeHtml($value))
                
                // clean the html on writing:
                ->write(fn (string $value): string => sanitizeHtml($value))
            // ...
        ];
    }
}

<?= $view->sanitizeHtml(html: $html) 

use Tobento\App\Crud\Field;

Field\Value::new(
    name: 'title',
    // you may set a label, otherwise name is used:
    label: 'TITLE',
);

use Tobento\App\Crud\Field;

Field\Value::new(name: 'title')->value('Lorem');

use Tobento\App\Crud\Field;

Field\Value::new(name: 'title')
    ->value('Lorem')
    ->indexable(true) // default false
    ->showable(true); // default false

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        //...

        Field\Text::new(name: 'sku')

            // disabled on index action:
            ->indexable(false)

            // disabled on create and store action:
            ->creatable(false)
            
            // disabled on edit, update, delete and any bulk actions such as bulk-edit and bulk-delete action:
            ->editable(false)
            
            // disabled on show action:
            ->showable(false),

        //...
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    yield Field\PrimaryId::new('id');

    if (in_array($action->name(), ['create', 'store'])) {
        yield Field\Text::new(name: 'sku');
    }

    yield Field\Text::new(name: 'title');
}

use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\Crud\Action;
use Tobento\App\Crud\Field;

protected function configureActions(): iterable|ActionsInterface
{
    return [
        Action\Index::new(title: 'Products')
            ->setFields(new Field\Fields(
                Field\PrimaryId::new('id'),
                Field\Text::new(name: 'sku'),
            )),
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'sku')
        
            // used for all actions:
            ->validate('

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'sku')
            ->validate('

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'title')
            ->translatable(),
    ];
}

use Tobento\Service\Repository\Storage\Column;
use Tobento\Service\Repository\Storage\Column\ColumnsInterface;

protected function configureColumns(): iterable|ColumnsInterface
{
    return [
        //...
        Column\Translatable::new(name: 'title'),
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'foo')
            ->storable(false),
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'foo')
            ->readonly()
            
            // or you may set only for specific actions:
            ->readonly(action: 'edit|update')
            
            // or you may use a closure (parameters are resolved by autowiring):
            ->readonly(
                readonly: function (ActionInterface $action, FieldInterface $field): bool {
                    return true;
                },
                action: 'edit|update'
            )
            
            ->disabled()
            
            // or you may set only for specific actions:
            ->disabled(action: 'edit|update')
            
            // or you may use a closure (parameters are resolved by autowiring):
            ->disabled(
                disabled: function (ActionInterface $action, FieldInterface $field): bool {
                    return true;
                },
                action: 'edit|update'
            )
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'foo')->group('Name'),
        Field\Text::new(name: 'bar')->group('Name'),
        
        Field\Text::new(name: 'baz')->group('Another Name'),
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'foo')
            ->

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'foo')
            ->optionalText('optional ...')
            // same as:
            ->optionalText(text: 'optional ...', action: 'create|edit')
            
            // or using different text per action:
            ->optionalText(text: 'optional ...', action: 'edit'),
    ];
}

use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\Crud\Field;

protected function configureFields(ActionInterface $action): iterable|FieldsInterface
{
    return [
        Field\Text::new(name: 'foo')
            ->infoText('Some info ...')
            // same as:
            ->infoText(text: 'Some info ...', action: 'create|edit')
            
            // or using different text per action:
            ->infoText(text: 'Some info ...', action: 'edit'),
    ];
}
file
app/config/file_storage.php
app/config/event.php
app/config/slugging.php
app/config/slugging.php
app/config/slugging.php