PHP code example of tobento / app-user-web

1. Go to this page and download the library: Download tobento/app-user-web 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-user-web example snippets


use Tobento\App\AppFactory;

// 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\User\Web\Boot\UserWeb::class);

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

'features' => [
    new Feature\Home(),
    
    // Or:
    new Feature\Home(
        // The view to render:
        view: 'user/home',

        // A menu name to show the home link or null if none.
        menu: 'main',
        menuLabel: 'Home',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\RateLimiter\Symfony\Registry\SlidingWindow;

'features' => [
    new Feature\Login(),
    
    // Or:
    new Feature\Login(
        // The view to render:
        view: 'user/login',

        // A menu name to show the login link or null if none.
        menu: 'header',
        menuLabel: 'Log in',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,
        
        // Specify a rate limiter:
        rateLimiter: new SlidingWindow(limit: 10, interval: '5 Minutes'),
        // see: https://github.com/tobento-ch/app-rate-limiter#available-rate-limiter-registries

        // Specify the identity attributes to be checked on login.
        identifyBy: ['email', 'username', 'smartphone', 'password'],
        // You may set a user verifier(s), see: https://github.com/tobento-ch/app-user#user-verifier
        /*userVerifier: function() {
            return new \Tobento\App\User\Authenticator\UserRoleVerifier('editor', 'author');
        },*/

        // The period of time from the present after which the auth token MUST be considered expired.
        expiresAfter: 1500, // int|\DateInterval

        // If you want to support remember. If set and the user wants to be remembered,
        // this value replaces the expiresAfter parameter.
        remember: new \DateInterval('P6M'), // null|int|\DateInterval

        // The message and redirect route if a user is authenticated.
        authenticatedMessage: 'You are logged in!',
        authenticatedRedirectRoute: 'home', // or null (no redirection)

        // The message shown if a login attempt fails.
        failedMessage: 'Invalid user or password.',

        // The redirect route after a successful login.
        successRoute: 'home',
            
        // The message shown after a user successfully log in.
        successMessage: 'Welcome back :greeting.', // or null

        // If set, it shows the forgot password link. Make sure the Feature\ForgotPassword is set too.
        forgotPasswordRoute: 'forgot-password.identity', // or null
        
        // The two factor authentication route.
        twoFactorRoute: 'twofactor.code.show',

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

'features' => [
    new Feature\Login(
        // If you want to support remember. If set and the user wants to be remembered,
        // this value replaces the expiresAfter parameter.
        remember: new \DateInterval('P6M'), //null|int|\DateInterval
    ),
],

use Tobento\App\User;

'middlewares' => [
    // You may uncomment it and set it on each route individually
    // using the User\Middleware\AuthenticationWith::class!
    User\Middleware\Authentication::class,
    User\Middleware\User::class,

    [User\Web\Middleware\RememberedToken::class, 'isRememberedAfter' => 1500],
    
    // or with date interval:
    [User\Web\Middleware\RememberedToken::class, 'isRememberedAfter' => new \DateInterval('PT2H')],
],

use Tobento\App\User\Middleware\Authenticated;

$app->route('GET', 'account-info', function() {
    return 'account';
})->middleware([
    Authenticated::class,
    'exceptVia' => 'remembered',
    'redirectRoute' => 'login',
]);

'features' => [
    new Feature\TwoFactorAuthenticationCode(),
    
    // Or:
    new Feature\TwoFactorAuthenticationCode(
        // The view to render:
        view: 'user/twofactor-code',

        // The period of time from the present after which the verification code MUST be considered expired.
        codeExpiresAfter: 300, // int|\DateInterval

        // The seconds after a new code can be reissued.
        canReissueCodeAfter: 60,

        // The message and redirect route if a user is unauthenticated.
        unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
        unauthenticatedRedirectRoute: 'home', // or null (no redirection)

        // The redirect route after a successful code verification.
        successRoute: 'home',

        // The message shown after a successful code verification.
        successMessage: 'Welcome back :greeting.', // or null

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\User\Web\Feature\Login;
use Tobento\App\User\UserInterface;

class CustomLoginFeature extends Login
{
    /**
     * Returns true if the user is our conditions here:
        if (in_array($user->getRoleKey(), ['business'])) {
            return true;
        }
        
        return false;
    }
}

'features' => [
    new CustomLoginFeature(
        //...
    ),
],

use Tobento\App\User\Middleware\Authenticated;

$app->route('GET', 'account-info', function() {
    return 'account';
})->middleware([
    Authenticated::class,
    'via' => 'twofactor-code',
    'redirectRoute' => 'home',
]);

use Tobento\App\User\Authentication\Token\TokenInterface;
use Tobento\App\User\Authenticator\TokenAuthenticator;
use Tobento\App\User\Authenticator\TokenVerifierInterface;
use Tobento\App\User\Exception\AuthenticationException;
use Tobento\App\User\UserInterface;
use Tobento\App\User\UserRepositoryInterface;
use Tobento\Service\Acl\AclInterface;

class CustomTokenAuthenticator extends TokenAuthenticator
{
    public function __construct(
        protected AclInterface $acl,
        protected UserRepositoryInterface $userRepository,
        protected null|TokenVerifierInterface $tokenVerifier = null,
    ) {}
    
    /**
     * Authenticate token.
     *
     * @param TokenInterface $token
     * @return UserInterface
     * @throws AuthenticationException If authentication fails.
     */
    public function authenticate(TokenInterface $token): UserInterface
    {
        $user = parent::authenticate($token);
        
        if ($token->authenticatedVia() === 'loginform-twofactor') {
            
            $role = $this->acl->getRole('registered');
            
            if (is_null($role)) {
                throw new AuthenticationException('Registered role not set up');
            }
            
            $user->setRole($role);
            $user->setRoleKey($role->key());
            $user->setPermissions([]); // clear user specific permissions too.
        }
        
        return $user;
    }
}

use Tobento\App\User\Authenticator;

'interfaces' => [
    // ...
    
    Authenticator\TokenAuthenticatorInterface::class => CustomTokenAuthenticator::class,
    
    // ...
],

'features' => [
    new Feature\Logout(),
    
    // Or:
    new Feature\Logout(
        // A menu name to show the logout link or null if none.
        menu: 'header',
        menuLabel: 'Log out',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,

        // The redirect route after a successful logout.
        redirectRoute: 'home',

        // The message and redirect route if a user is unauthenticated.
        unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
        unauthenticatedRedirectRoute: 'home', // or null (no redirection)

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

'features' => [
    new Feature\ForgotPassword(),
    
    // Or:
    new Feature\ForgotPassword(
        viewIdentity: 'user/forgot-password/identity',
        viewReset: 'user/forgot-password/reset',

        // Specify the identity attributes to be checked on identity.
        identifyBy: ['email', 'username', 'smartphone'],
        // You may set a user verifier(s), see: https://github.com/tobento-ch/app-user#user-verifier
        /*userVerifier: function() {
            return new \Tobento\App\User\Authenticator\UserRoleVerifier('editor', 'author');
        },*/
        
        // The period of time from the present after which the verification token MUST be considered expired.
        tokenExpiresAfter: 300, // int|\DateInterval
        
        // The seconds after a new token can be reissued.
        canReissueTokenAfter: 60,
            
        // The message shown if an identity attempt fails.
        identityFailedMessage: 'Invalid name or user.',

        // The message and redirect route if a user is authenticated.
        authenticatedMessage: 'You are logged in!',
        authenticatedRedirectRoute: 'home', // or null (no redirection)

        // The redirect route after a successful password reset.
        successRedirectRoute: 'home',
        
        // The message shown after a successful password reset.
        successMessage: 'Your password has been reset!', // or null

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\User\Web\Feature\ForgotPassword;
use Tobento\App\User\Web\TokenInterface;
use Tobento\App\User\Web\Notification;
use Tobento\App\User\UserInterface;
use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Notifier\UserRecipient;
use Tobento\Service\Routing\RouterInterface;

class CustomForgotPasswordFeature extends ForgotPassword
{
    protected function sendLinkNotification(
        TokenInterface $token,
        UserInterface $user,
        NotifierInterface $notifier,
        RouterInterface $router,
    ): void {
        $notification = new Notification\ResetPassword(
            token: $token,
            url: (string)$router->url('forgot-password.reset', ['token' => $token->id()]),
        );
        
        // The receiver of the notification:
        $recipient = new UserRecipient(user: $user);

        // Send the notification to the recipient:
        $notifier->send($notification, $recipient);
    }
}

'features' => [
    new CustomForgotPasswordFeature(),
],

'features' => [
    new Feature\Register(),
    
    // Or:
    new Feature\Register(
        // The view to render:
        view: 'user/register',

        // A menu name to show the register link or null if none.
        menu: 'header',
        menuLabel: 'Register',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,

        // The message and redirect route if a user is authenticated.
        authenticatedMessage: 'You are logged in!',
        authenticatedRedirectRoute: 'home',

        // The default role key for the registered user.
        roleKey: 'registered',

        // The redirect route after a successful registration.
        successRedirectRoute: 'login',
        // You may redirect to the verification account page
        // see: https://github.com/tobento-ch/app-user-web#account-verification

        // If true, user has the option to subscribe to the newsletter.
        newsletter: false,

        // If a terms route is specified, users need to agree terms and conditions.
        termsRoute: null,
        /*termsRoute: static function (RouterInterface $router): string {
            return (string)$router->url('blog.show', ['key' => 'terms']);
        },*/

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\User\Web\Feature\Register;

class CustomRegisterFeature extends Register
{
    protected function validationRules(): array
    {
        return [
            'user_type' => 'string',
            'address.fistname' => '

'features' => [
    new CustomRegisterFeature(
        // specify your custom view if (step 1.A):
        view: 'user/custom-register',
        
        // No need to change the default view for (step 1.B)
        view: 'user/register',
        //...
    ),
],

use Tobento\App\User\Web\Feature\Register;
use Tobento\Service\Acl\AclInterface;
use Tobento\Service\Validation\ValidationInterface;

class CustomRegisterFeature extends Register
{
    protected function determineRoleKey(AclInterface $acl, ValidationInterface $validation): string
    {
        return match ($validation->valid()->get('user_type', '')) {
            'business' => $acl->hasRole('business') ? 'business' : $this->roleKey,
            default => $this->roleKey,
        };
    }
}

'features' => [
    new CustomRegisterFeature(
        //...
    ),
],

use Tobento\App\User\Web\Listener\AutoLoginAfterRegistration;

'listeners' => [
    \Tobento\App\User\Web\Event\Registered::class => [
        AutoLoginAfterRegistration::class,
        
        // Or you may set the expires after:
        [AutoLoginAfterRegistration::class, ['expiresAfter' => 1500]], // in seconds
        [AutoLoginAfterRegistration::class, ['expiresAfter' => new \DateInterval('PT1H')]],
    ],
],

'features' => [
    new Feature\Register(
        // redirect users to the profile page
        // after successful registration:
        successRedirectRoute: 'profile.edit',
    ),
],

'listeners' => [
    \Tobento\App\User\Web\Event\Registered::class => [
        \Tobento\App\User\Web\Listener\AutoLoginAfterRegistration::class,
    ],
],

'features' => [
    new Feature\Register(
        // redirect users to the verification account page
        // after successful registration:
        successRedirectRoute: 'verification.account',
    ),
    
    // make sure the verification feature is set:
    Feature\Verification::class,
],

use Tobento\App\User\Web\Feature\Profile;
use Tobento\App\AppInterface;
use Tobento\App\User\Middleware\Authenticated;
use Tobento\App\User\Middleware\Verified;

class CustomProfileFeature extends Profile
{
    protected function configureMiddlewares(AppInterface $app): array
    {
        return [
            // The Authenticated::class middleware protects routes from unauthenticated users:
            [
                Authenticated::class,

                // you may specify a custom message to show to the user:
                'message' => $this->unauthenticatedMessage,

                // you may specify a message level:
                //'messageLevel' => 'notice',

                // you may specify a route name for redirection:
                'redirectRoute' => $this->unauthenticatedRedirectRoute,
            ],
            // The Verified::class middleware protects routes from unverified users:
            [
                Verified::class,

                // you may specify a custom message to show to the user:
                'message' => 'You have insufficient rights to access the requested resource!',

                // you may specify a message level:
                'messageLevel' => 'notice',

                // you may specify a route name for redirection:
                'redirectRoute' => 'verification.account',
            ],
        ];
    }
}

'features' => [
    new CustomProfileFeature(
        //...
    ),
],

use Tobento\App\User\Web\Feature\Register;
use Tobento\App\User\UserInterface;

class CustomRegisterFeature extends Register
{
    protected function configureSuccessRedirectRoute(UserInterface $user): string
    {
        if (in_array($user->getRoleKey(), ['business'])) {
            return 'verification.account';
        }
        
        return 'login';
    }
}

use Tobento\App\User\Web\Listener\AutoLoginAfterRegistration;

'listeners' => [
    \Tobento\App\User\Web\Event\Registered::class => [
        [AutoLoginAfterRegistration::class, ['userRoles' => ['business']]],
    ],
],

'features' => [
    new Feature\Register(
        // If a terms route is specified, users need to agree terms and conditions.
        termsRoute: 'your.terms.route.name',
        
        // Or you may use router directly:
        termsRoute: static function (RouterInterface $router): string {
            return (string)$router->url('blog.show', ['key' => 'terms']);
        },
    ),
],

use Tobento\App\Spam\Factory;

'detectors' => [
    'register' => new Factory\Composite(
        new Factory\Honeypot(inputName: 'hp'),
        new Factory\MinTimePassed(inputName: 'mtp', milliseconds: 1000),
    ),
]

'features' => [
    new Feature\Notifications(),
    
    // Or:
    new Feature\Notifications(
        // The view to render:
        view: 'user/notifications',

        // The notifier storage channel used to retrieve notifications.
        notifierStorageChannel: 'storage',

        // A menu name to show the notifications link or null if none.
        menu: 'main',
        menuLabel: 'Notifications',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,

        // The message and redirect route if a user is unauthenticated.            
        unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
        unauthenticatedRedirectRoute: 'login', // or null (no redirection)

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Notifier\Notification;
use Tobento\Service\Notifier\Recipient;

class SomeService
{
    public function send(NotifierInterface $notifier): void
    {
        // Create a Notification that has to be sent:
        // using the "email" and "sms" channel
        $notification = new Notification(
            subject: 'New Invoice',
            content: 'You got a new invoice for 15 EUR.',
            channels: ['mail', 'sms', 'storage'],
        );
        
        // with specific storage message. Will be displayed on the notifications page:
        $notification->addMessage('storage', new Message\Storage([
            'message' => 'You received a new order.',
            'action_text' => 'View Order',
            'action_route' => 'orders.view',
            'action_route_parameters' => ['id' => 55],
        ]));

        // The receiver of the notification:
        $recipient = new Recipient(
            email: '[email protected]',
            phone: '15556666666',
            id: 'unique-user-id',
        );

        // Send the notification to the recipient:
        $notifier->send($notification, $recipient);
    }
}

use Tobento\App\AppInterface;
use Tobento\App\User\Web\Feature\Notifications;
use Tobento\Service\Notifier\ChannelsInterface;
use Tobento\Service\Notifier\Storage;
use Psr\SimpleCache\CacheInterface;

class CustomNotificationsFeature extends Notifications
{
    /**
     * Returns the user's unread notifications count for the menu badge.
     *
     * @param ChannelsInterface $channels
     * @param UserInterface $user
     * @param AppInterface $app
     * @return int
     */
    protected function getUnreadNotificationsCount(
        ChannelsInterface $channels,
        UserInterface $user,
        AppInterface $app,
    ): int {
        $channel = $channels->get(name: $this->notifierStorageChannel);
        
        if (!$channel instanceof Storage\Channel) {
            return 0;
        }
        
        $key = sprintf('unread_notifications_count:%s', (string)$user->id());
        
        $cache = $app->get(CacheInterface::class);
        
        if ($cache->has($key)) {
            return $cache->get($key);
        }
        
        $count = $channel->repository()->count(where: [
            'recipient_id' => $user->id(),
            'read_at' => ['null'],
        ]);
        
        $cache->set($key, $count, 60);
        
        return $count;
    }
}

'features' => [
    new CustomNotificationsFeature(
        //...
    ),
],

use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;

$schedule->task(
    (new Task\CommandTask(
        command: 'notifications:clear --read-only',
    ))
    // schedule task:
    ->cron(Generator::create()->weekly())
);

'features' => [
    new Feature\Profile(),
    
    // Or:
    new Feature\Profile(
        // The view to render:
        view: 'user/profile/edit',

        // A menu name to show the profile link or null if none.
        menu: 'main',
        menuLabel: 'Profile',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,

        // The message and redirect route if a user is unauthenticated.            
        unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
        unauthenticatedRedirectRoute: 'login', // or null (no redirection)

        // If true, it displays the channel verification section to verify channels.
        // Make sure the Verification Feature is enabled.
        channelVerifications: true,

        // The redirect route after a successfully account deletion.
        successDeleteRedirectRoute: 'home',

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\User\Web\Feature\Profile;
use Tobento\App\User\UserInterface;

class CustomProfileFeature extends Profile
{
    /**
     * Returns the validation rules for updating the user's profile information.
     *
     * @param UserInterface $user
     * @return array
     */
    protected function validationUpdateRules(UserInterface $user): array
    {
        // your rules:
        $rules = [
            'user_type' => 'string',
            'address.fistname' => '

'features' => [
    new CustomProfileFeature(
        // specify your custom view if (step 1.A):
        view: 'user/profile/custom-edit',
        
        // No need to change the default view for (step 1.B)
        view: 'user/profile/edit',
        //...
    ),
],

use Tobento\App\User\Web\Feature\Profile;
use Tobento\App\User\UserInterface;
use Tobento\App\Notifier\AvailableChannelsInterface;

class CustomProfileFeature extends Profile
{
    /**
     * Configure the available verification channels to display.
     *
     * @param AvailableChannelsInterface $channels
     * @param UserInterface $user
     * @return AvailableChannelsInterface
     */
    protected function configureAvailableChannels(
        AvailableChannelsInterface $channels,
        UserInterface $user,
    ): AvailableChannelsInterface {
        if (! $this->channelVerifications) {
            // do not display any at all:
            return $channels->only([]);
        }
        
        return $channels->only(['mail', 'sms']);
    }
}

'features' => [
    new CustomProfileFeature(
        //...
    ),
],

'features' => [
    new Feature\ProfileSettings(),
    
    // Or:
    new Feature\ProfileSettings(
        // The view to render:
        view: 'user/profile/settings',

        // A menu name to show the profile settings link or null if none.
        menu: 'main',
        menuLabel: 'Profile Settings',
        // A menu parent name (e.g. 'user') or null if none.
        menuParent: null,

        // The message and redirect route if a user is unauthenticated.            
        unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
        unauthenticatedRedirectRoute: 'login', // or null (no redirection)

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\User\Web\Feature\ProfileSettings;
use Tobento\App\User\UserInterface;

class CustomProfileSettingsFeature extends ProfileSettings
{
    /**
     * Returns the validation rules for updating the user's profile settings.
     *
     * @param UserInterface $user
     * @return array
     */
    protected function validationRules(UserInterface $user): array
    {
        // your rules:
        $rules = [
            'settings.something' => '

'features' => [
    new CustomProfileSettingsFeature(
        // specify your custom view if (step 1.A):
        view: 'user/profile/custom-settings',
        
        // No need to change the default view for (step 1.B)
        view: 'user/profile/settings',
        //...
    ),
],

use Tobento\App\User\Web\Feature\ProfileSettings;
use Tobento\App\User\UserInterface;
use Tobento\App\Notifier\AvailableChannelsInterface;

class CustomProfileSettingsFeature extends ProfileSettings
{
    /**
     * Configure the available channels.
     *
     * @param AvailableChannelsInterface $channels
     * @param UserInterface $user
     * @return AvailableChannelsInterface
     */
    protected function configureAvailableChannels(
        AvailableChannelsInterface $channels,
        UserInterface $user,
    ): AvailableChannelsInterface {
        return $channels
            ->only(['mail', 'sms', 'storage'])
            ->withTitle('storage', 'Account')
            ->sortByTitle();
        
        // Or you may return no channels at all:
        return $channels->only([]);
    }
}

'features' => [
    new CustomProfileSettingsFeature(
        //...
    ),
],

'features' => [
    new Feature\Verification(),
    
    // Or:
    new Feature\Verification(
        // The view to render:
        viewAccount: 'user/verification/account',
        viewChannel: 'user/verification/channel',

        // The period of time from the present after which the verification code MUST be considered expired.
        codeExpiresAfter: 300, // int|\DateInterval

        // The seconds after a new code can be reissued.
        canReissueCodeAfter: 60,
            
        // The message and redirect route if a user is unauthenticated.            
        unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
        unauthenticatedRedirectRoute: 'login', // or null (no redirection)

        // The redirect route after a verified channel.
        verifiedRedirectRoute: 'home',

        // If true, routes are being localized.
        localizeRoute: false,
    ),
],

use Tobento\App\User\Web\Feature\Verification;
use Tobento\App\User\UserInterface;
use Tobento\App\Notifier\AvailableChannelsInterface;

class CustomVerificationFeature extends Verification
{
    /**
     * Configure the available channels.
     *
     * @param AvailableChannelsInterface $channels
     * @param UserInterface $user
     * @return AvailableChannelsInterface
     */
    protected function configureAvailableChannels(
        AvailableChannelsInterface $channels,
        UserInterface $user,
    ): AvailableChannelsInterface {
        //return $channels->only(['mail', 'sms']); // default
        
        return $channels->only(['mail', 'sms', 'chat/slack']);
    }
    
    /**
     * Returns the notifier channel for the given verification channel.
     *
     * @param string $channel
     * @return null|string
     */
    protected function getNotifierChannelFor(string $channel): null|string
    {
        return match ($channel) {
            'email' => 'mail',
            'smartphone' => 'sms',
            'slack' => 'chat/slack',
            default => null,
        };
    }
    
    /**
     * Determine if the channel can be verified.
     *
     * @param string $channel
     * @param AvailableChannelsInterface $channels
     * @param null|UserInterface $user
     * @return bool
     */
    protected function canVerifyChannel(string $channel, AvailableChannelsInterface $channels, null|UserInterface $user): bool
    {
        if (is_null($user) || !$user->isAuthenticated()) {
            return false;
        }
        
        if (! $channels->has((string)$this->getNotifierChannelFor($channel))) {
            return false;
        }
        
        return match ($channel) {
            'email' => !empty($user->email()) && ! $user->isVerified([$channel]),
            'smartphone' => !empty($user->smartphone()) && ! $user->isVerified([$channel]),
            'slack' => !empty($user->setting('slack')) && ! $user->isVerified([$channel]),
            default => false,
        };
    }
}

'features' => [
    new CustomVerificationFeature(
        //...
    ),
],

use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;

$schedule->task(
    (new Task\CommandTask(
        command: 'user-web:clear-tokens',
    ))
    // schedule task:
    ->cron(Generator::create()->weekly())
);

use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;

$schedule->task(
    (new Task\CommandTask(
        command: 'auth:purge-tokens',
    ))
    // schedule task:
    ->cron(Generator::create()->weekly())
);

use Tobento\App\User\Web\Event;

use Tobento\App\User\Web\Event;

class UserNewletterSubscriber
{
    public function subscribe(Event\Registered $event): void
    {
        if ($event->user()->newsletter()) {
            // subscribe...
        }
    }

    public function resubscribe(Event\UpdatedProfile $event): void
    {
        if ($event->user()->email() !== $event->oldUser()->email()) {
            // unsubscribe user with the old email address...
            // subscribe user with the new email address...
        }
    }
    
    public function subscribeOrUnsubscribe(Event\UpdatedProfileSettings $event): void
    {
        if ($event->user()->newsletter()) {
            // subscribe...
        } else {
            // unsubscribe...
        }
    }
    
    public function unsubscribe(Event\DeletedAccount $event): void
    {
        // just unsubscribe...
    }
}

'listeners' => [
    // Specify listeners without event:
    'auto' => [
        UserNewletterSubscriber::class,
    ],
],

use Tobento\App\User\Web\Notification;
use Tobento\App\User\Web\PinCodeVerificator;
use Tobento\App\User\Web\TokenInterface;
use Tobento\App\User\UserInterface;
use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Routing\RouterInterface;

class CustomPinCodeVerificator extends PinCodeVerificator
{
    protected function createNotification(
        TokenInterface $token,
        UserInterface $user,
        array $channels,
    ): NotificationInterface {
        return (new Notification\VerificationCode(token: $token))->channels($channels);
    }
}

use Tobento\App\User\Web;

'interfaces' => [
    Web\PinCodeVerificatorInterface::class => CustomPinCodeVerificator::class,
],

'features' => [
    new Feature\Home(
        localizeRoute: true,
    ),
    new Feature\Register(
        localizeRoute: true,
    ),
],
app/config/user_web.php
app/config/user_web.php
app/config/user.php
config/user_web.php
config/user.php
views/user/
custom-register.php
config/event.php
config/user_web.php
config/event.php
config/user_web.php
config/event.php
config/user_web.php
app/config/spam.php
views/user/
profile/custom-edit.php
views/user/
profile/custom-settings.php
app/config/notifier.php

php ap user-web:clear-tokens

php ap auth:purge-tokens
app/config/notifier.php
config/event.php
app/config/language.php
en