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 [
            new Field\PrimaryId('id'),
            new Field\Text('sku'),
            //...
        ];
    }
    
    /**
     * Returns the configured actions.
     *
     * @return iterable<ActionInterface>|ActionsInterface
     */
    protected function configureActions(): iterable|ActionsInterface
    {
        return [
            new Action\Index(title: 'Products'),
            //...
        ];
    }
    
    /**
     * Returns the configured filters.
     *
     * @param ActionInterface $action
     * @return iterable<FilterInterface>|FiltersInterface
     */
    protected function configureFilters(ActionInterface $action): iterable|FiltersInterface
    {
        return [
            new Filter\Columns()->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;

class ProductsController extends AbstractCrudController
{
    /**
     * Determines if action is processable.
     *
     * @param ActionInterface $action
     * @return void
     * @throws \Throwable
     */
    public function isActionProcessable(ActionInterface $action): void
    {
        if ($action->name() === 'edit') {
            throw new \Tobento\App\Http\Exception\ForbiddenException();
        }
        
        parent::isActionProcessable(action: $action);
    }
}

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 [
            new Field\PrimaryId('id'),
            new Field\Text(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 [
            new Action\Index(title: 'Products'),
            new Action\Create(title: 'New product'),
            new Action\Store(),
            new Action\Edit(title: 'Edit product'),
            new Action\Update(),
            new Action\Copy(title: 'Copy product'),
            new Action\Show(),
            new Action\Delete(),
            new Action\BulkDelete(),
            new Action\BulkEdit(),
        ];
    }
}

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 [
            new Filter\Columns()->open(false),
            new Filter\FieldsSortOrder(),
            ...new Filter\Fields()
               ->group('field')
               ->fields($action->fields())
               ->toFilters(),
            new Filter\PaginationItemsPerPage()->open(false),
            
            // must be added last!
            new Filter\Pagination(),
        ];
    }
}

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

protected function configureFilters(ActionInterface $action): iterable|FiltersInterface
{
    yield new Filter\Columns()->open(false);

    if (in_array($action->name(), ['custom'])) {
        yield new Filter\FieldsSortOrder();
    }
}

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

protected function configureActions(): iterable|ActionsInterface
{
    return [
        new Action\Index(title: 'Products')
            ->setFilters(new Filter\Filters(
                new Filter\Columns()->open(false),
            )),
    ];
}

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,
            
            // Register only specific actions:
            only: ['index', 'show'],
            
            // Or exclude certain actions:
            except: ['bulk'],
            
            // Apply middleware to all generated routes:
            middleware: [
                SomeMiddleware::class,
            ],
            
            // Generate localized routes (e.g. /en/products, /de/products):
            localized: true,
            
            // Add route parameter constraints for the "id" parameter (default):
            whereId: '[a-z0-9]+',
        );
    }
}

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' => [
                        // Read permissions:
                        'products.index' => 'products',
                        'products.show' => 'products',
                        
                        // Create permissions:
                        'products.create' => 'products.create',
                        'products.store' => 'products.create',
                        'products.copy' => 'products.create',
                        
                        // Update permissions:
                        'products.edit' => 'products.edit',
                        'products.update' => 'products.edit',
                        
                        // Delete permissions:
                        'products.delete' => 'products.delete',
                        
                        // Bulk actions (e.g. BulkDelete, BulkEdit):
                        // You may combine permissions using "|"
                        'products.bulk' => 'products.edit|products.delete',
                    ],
                ]
            ],
        );
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\Service\Acl\AclInterface;

class ProductController extends AbstractCrudController
{
    public const RESOURCE_NAME = 'products';

    public function __construct(
        ProductRepository $repository,
        protected AclInterface $acl,
    ) {
        $this->repository = $repository;
    }

    protected function configureActions(): iterable|ActionsInterface
    {
        // Read
        if ($this->acl->can('products')) {
            yield new Action\Index('Products');
            yield new Action\Show();
        }

        // Create
        if ($this->acl->can('products.create')) {
            yield new Action\Create();
            yield new Action\Store();
            yield new Action\Copy();
        }

        // Edit
        if ($this->acl->can('products.edit')) {
            yield new Action\Edit();
            yield new Action\Update();
        }

        // Delete
        if ($this->acl->can('products.delete')) {
            yield new Action\Delete();
        }
    }
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\ActionProcessorInterface;
use Tobento\App\Crud\CrudWriteRepository;
use Tobento\Service\Repository\WriteRepositoryInterface;

$repository = new CrudWriteRepository(
    controller: $controller, // AbstractCrudController
    actionProcessor: $actionProcessor, // ActionProcessorInterface
);

var_dump($repository instanceof WriteRepositoryInterface);
// bool(true)

use Tobento\App\Crud\Entity\EntityInterface;
use Tobento\App\Crud\Exception\ValidationException;
use Tobento\Service\Repository\RepositoryCreateException;

try {
    $entity = $repository->create([
        'title' => 'Lorem ipsum',
    ]);
    
    var_dump($entity instanceof EntityInterface);
    // bool(true)
} catch (RepositoryCreateException $e) {
    // do something ...
    if ($e->getPrevious() instanceof ValidationException) {
        $errorsArray = $e->getPrevious()->validation()->errors()->toArray();
    }
}

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

$repository = $repository->onlyFields('id', 'title');

$repository = $repository->exceptFields('sku');

$repository = $repository->withFields(function(FieldsInterface $fields, ActionInterface $action): FieldsInterface {
    return $fields;
});

$repository = $repository->onlyActions('store', 'update');

use Tobento\App\Crud\Field;

new Field\Buttons(name: 'btns')
    ->buttons(
        new Button\Button(label: 'Save', group: 'entity')
            ->name('save')
            ->attr(name: 'name', value: 'next_action')
            ->attr(name: 'value', value: 'edit')
            ->attr(name: 'data-loading', value: 'true')
            ->ajaxAction('Record saved successfully.')
            ->primary(),
        new Button\Button(label: 'Save & Close', group: 'entity')
            ->name('close')
            ->attr(name: 'data-loading', value: 'true')
            ->ajaxAction(),
    )
    
    // Alignment options:
    ->alignLeft() // default if none is set.
    ->alignRight()
    ->alignCenter()
    
    // Render buttons as a field layout:
    ->displayAsField();

use Tobento\App\Crud\Field;

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

new Field\Checkboxes('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());

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

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

new Field\Checkboxes('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;

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

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

new Field\Files(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;

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

use Tobento\App\Crud\Field;

new Field\Files(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;

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

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

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

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

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

use Tobento\Service\Upload\Validator;
use Tobento\Service\Upload\ValidatorInterface;

new Field\FileSource('image')
    ->validator(static function(): ValidatorInterface {
        return new Validator\General(
            allowedExtensions: ['jpg'],
            strictFilenameCharacters: true,
            maxFilenameLength: 200,
            maxFileSizeInKb: 2000,
        );
    });

use Tobento\App\Media\Upload\ImageProcessor;
use Tobento\App\Media\Upload\Writer\SvgSanitizer;
use Tobento\Service\FileStorage\StorageInterface;
use Tobento\Service\Upload\CopyFileWrapper;
use Tobento\Service\Upload\FileStorageWriter;
use Tobento\Service\Upload\FileStorageWriterInterface;
use Tobento\Service\Upload\Writer;

new Field\FileSource('image')
    ->fileStorageWriter(static function(StorageInterface $storage, mixed $inputFile): FileStorageWriterInterface {
        $writers = [];

        // Only process images for real uploads
        if (! $inputFile instanceof CopyFileWrapper) {
            $writers[] = new Writer\Image(
                imageProcessor: new ImageProcessor(
                    actions: [
                        'orientate' => [],
                        'resize' => ['width' => 2000],
                    ],
                ),
            );

            $writers[] = new SvgSanitizer();
        }
        
        return new FileStorageWriter(
            storage: $storage,
            filenames: FileStorageWriter::ALNUM, // RENAME, ALNUM, KEEP
            duplicates: FileStorageWriter::RENAME, // RENAME, OVERWRITE, DENY
            folders: FileStorageWriter::ALNUM, // or KEEP
            folderDepthLimit: 5,
            writers: $writers,
        );
    });

use Tobento\Service\Upload\UploadedFileFactoryInterface;

new Field\FileSource('image')
    ->modifyInputValue(
        modifier: function(
            mixed $value,
            Field\FileSource $field,
            UploadedFileFactoryInterface $uploadedFileFactory
        ): mixed {
            // Convert a remote URL into an UploadedFile instance
            if (is_string($value)) {
                return $uploadedFileFactory->createFromRemoteUrl($value);
            }

            return $value;
        },
        action: 'store|update', // default
    );

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

new Field\FileSource('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);

new Field\FileSource('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']],
    ],
],

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

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

use Tobento\App\Crud\Event;

'listeners' => [
    \Tobento\App\Crud\Event\FileSourceDeleted::class => [
        \Tobento\App\Crud\Listener\DeletesGeneratedPictures::class,
    ],
],

use Tobento\App\Crud\Field;

new Field\Group(name: 'seo')
    // define the fields:
    ->fields(
        new Field\Text('meta_title', 'Meta Title')->translatable(),
        new Field\Text('meta_desc', 'Meta Description')->translatable(),
    )
    // you may group the fields, otherwise groups from the defined fields are used:
    ->group('SEO')
    
    // you may prepend the group name to each field:
    ->prependGroupName()

    // you may display the fields as field layout:
    ->displayAsField()
    
    // you may display the fields as card layout:
    ->displayAsCard()
    
    // you may display the group label when displaying as card:
    ->displayLabel();

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

class SeoFields extends Field\Group
{
    protected function configure(): void
    {
        $this->group('Seo');
        $this->fields();
    }

    public function fields(FieldInterface ...$fields): static
    {
        $this->fields = [
            new Field\Text('meta_title', 'Meta Title')->translatable(),
            new Field\Text('meta_desc', 'Meta Description')->translatable(),
        ];
        
        return $this;
    }
}

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

new SeoFields(name: 'seo')
    // you may rename fields:
    ->renameFields(['meta_desc' => 'meta_description'])
    
    // you may remove fields:
    ->removeField('meta_title')
    
    // you may modify fields:
    ->modifyField(name: 'meta_title', modifier: function(FieldInterface $field): void {
        $field->translatable(false);
    })
    
    // you may modify fields:
    ->modifyFields(modifier: function(FieldsInterface $fields): FieldsInterface {
        // modify ...
        return $fields;
    });

use Tobento\App\Crud\Field;

new Field\Html(
    name: 'title',
);

use Tobento\App\Crud\Field;

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

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

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

new Field\Items('prices')
    // you may group the fields
    ->group('Prices') // set before defining fields!
    
    // define the fields per item:
    ->fields(
        new Field\Text('price_net', 'Price Net')
            ->type('number')
            ->attributes(['step' => 'any'])
            ->validate('decimal'),
        new Field\Text('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;
use Tobento\App\Crud\Field\FieldInterface;

new Field\Items('items')
    ->onCreateField(
        callback: function(
            FieldInterface $template,
            int $index,
            array $rowInput
        ): null|FieldInterface {

            // Example: add info text based on the row index
            $template->infoText('Item #'.$index);

            // Example: conditionally replace the field
            if (($rowInput['type'] ?? null) === 'special') {
                return new Field\Text(name: 'special');
            }

            // Return the modified template field
            return $template;
        }
    );

use Tobento\App\Crud\Field;

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

use Tobento\Service\Repository\RepositoryInterface;

new Field\Options('categories')
    ->repository(CategoriesRepository::class) // class-string|RepositoryInterface
    
    // you may add base where queries:
    ->baseWhere(['type' => 'blog'])
    
    // 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;

new Field\Options('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;

new Field\Options('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!
            ->image(
                image: $item->get('image', []),
                view: $view,
            );
    });

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

new Field\Options('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',
    )

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

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

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

new Field\Radios('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;

new Field\Radios('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;

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

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

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

use Tobento\App\Crud\Field;

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

new Field\Select('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());

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

new Field\Select('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;

new Field\Select('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;

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

new Field\Select('colors')->optionAttributes([
    // all options using wildcard:
    '*' => ['data-foo' => 'value'],
    // specific option using option value:
    'blue' => ['data-bar' => 'value'],
]);

new Field\Select('colors')->optgroupAttributes(['data-foo' => 'value']);

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

use Tobento\App\Crud\Field;

new Field\SingleOptions(
    name: 'category',
    // you may set a label, otherwise name is used:
    label: 'Category',
);

use Tobento\Service\Repository\RepositoryInterface;

new Field\SingleOptions('category')
    ->repository(CategoriesRepository::class) // class-string|RepositoryInterface
    
    // you may add base where queries:
    ->baseWhere(['type' => 'blog'])
    
    // 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;

new Field\SingleOptions('category')
    ->toOption(function(object $item, ViewInterface $view, Field\SingleOptions $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;

new Field\SingleOptions('category')
    ->toOption(function(object $item, ViewInterface $view, Field\SingleOptions $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!
            ->image(
                image: $item->get('image', []),
                view: $view,
            );
    });

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

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

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

new Field\SingleOptions('categories')
    ->displayAsModal();

new Field\SingleOptions('category')
    ->validate('

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

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

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

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

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

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

use Tobento\App\Crud\Field;

new Field\Text(name: 'email')->type('email');
file
app/config/file_storage.php
app/config/event.php
app/config/event.php
app/config/slugging.php
app/config/slugging.php
app/config/slugging.php
php
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 [
            // ...
            new Column\Text('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))
            // ...
        ];
    }
}
php
<?= $view->sanitizeHtml(html: $html)