Laravel Envoyer Notifications in Zoho Cliq

I recently switched a project from a home-grown deployment script to Laravel Envoyer. While the homegrown script could maybe have been adapted it would have taken time, and had to be maintained, and Envoyer offers some useful extras like Slack, Discord, and Microsoft Teams integration, and heartbeat monitoring.

Except we don’t use Slack, Discord, or Teams, we use Zoho‘s Slack competitor, Cliq.

Comparing Slack’s web hooks with Cliq we see that a bot is needed in Cliq, but making one is super-easy, on the order of four or five clicks.

Once there is a bot it can receive web hooks. Figuring out the right way to provide an authentication token was weird, there is not clear documentation, but the reply from Eric Hirst in in the Zoho forum thread announcing Cliq bots has a solution that works.

Now we can receive web hooks, I tried putting the bot’s web hook URL in for both Slack notifications and Discord notifications in Envoyer, and I got notifications. Both systems expect a POSTed JSON object, so that’s what they get. The Discord one is simpler one of the properties is simply markdown text, which Cliq understands, so the Cliq incoming web hook handler needs to grab that markdown text and return it so the bot will post the message in Cliq.

// Configure the bot's incoming webhook URL in any third-party service to trigger this handler.
response = Map();
message = body.toMap();
if(message.containKey('content'))
{
	response.put("text",message.get('content'));

	// If you want the bot to post to one of your channels, and want it to appear
	// as the bot posting, add the bot info like this. Otherwise it the message 
	// will be "from" the person who owns the auth token, with a small "bot" flag next to it.
	response.put('bot',{"name":"Envoyer","image":"https://envoyer.io/img/favicons/apple-touch-icon-120x120.png"});

	// If you want to post to a channel this is how. If you don't do this the bot
	// will simply post the message to any chats it has open.
	zoho.cliq.postToChannel('general',response);
}
return response;

The beauty of simply dumping the markdown into the chat is that this simple code handles all notifications from Envoyer, including successful & failed deployments and heartbeat notifications. You can look for specific text and alter your notification if you want, (I have a big headline for successful deployments), but it’s not needed.

It would be easier to test this if there was a way to trigger a test notification in Envoyer, but Envoyer isn’t really something to tinker with, it’s supposed to just work. It would also be great if Envoyer could support Zoho Cliq directly, but I’m not sure many people in the Laravel community are using Zoho.

So now we have Envoyer notifications in Zoho Cliq in our organization. If you’re one of the few Laravel / Zoho unicorns out there like me you can have them too.

FormRequest object Validation for Laravel Nova Resources

Laravel Nova gives us developers a relatively easy way to set up an administration panel for a website or app, but it’s been publicly available for less than a year and is only at version 1.3.1, so there are a few rough edges, like the inability to use FormRequest objects for validation when updating models, (called “Resources” in Nova parlance).

FormRequest objects are a way to detach form validation from specific controllers or routes so that the validation logic can be re-used, for example by using the same FormRequest object to validate updates made on a website or through an API. But they’re not supported by Nova so validation code needs to be re-written for Nova resources, or does it?

Comparing Laravel to Nova Validation

Validation rules in Nova are the same as the rest of Laravel, with the at least one tiny change in syntax, but instead of being passed around as one big list, each field has validation attached directly to it when defining the field. Here is the difference between rules in a FormRequest, (it’s the same whether using a FormRequest, Validator object, or in a controller), and rules for Nova fields:

Rules using a FormRequest

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StorePostRequest extends FormRequest
{
     // ....
	
     /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // note all rules are returned as an array
    	return [
	        'title' => 'required|unique:posts|max:255',
	        'body' => 'required',
	    ];
    }

    // ....
}

Rules using Laravel Nova

namespace App\Nova;

use Laravel\Nova\Fields\ID;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;


class Post extends Resource
{

    // ...
	
    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fields(Request $request)
    {

    	return [

    		// ...
    		
                // In Nova the rules are attached to fields, which themselves are an array.
    		Text::make('Title')->rules('required', 'unique:posts', 'max:255'),
    		Textarea::make('Body')->rules('required')

    		// ...

    	];
    }

    // ...
}

The “Laravel Way” to use a FormRequest for validation would be to type hint the $request passed to the fields method like this, (but it doesn’t work, apparently ):

namespace App\Nova;

use Illuminate\Http\Request;
use App\Http\Requests\StorePostRequest;


class Post extends Resource
{
    // ...

    // This type-hint produces a fatal error
    public function fields(StorePostRequest $request)
    {
    	return [
    		// ...
    	];
    }

    // ...
}

I’m actually not sure why the error is produced. The error I get says that Post@fields expects an instance of Illuminate\Http\Request, and that StorePostRequest isn’t one, but I am pretty sure it is an instance of Illuminate\Http\Request. Either way it doesn’t work, and that’s the expected behaviour, so a workaround is needed.

A Workaround

One way to only write validation rules once is to move them into a static method on the FormRequest object that has an optional $rule parameter to fetch one rule at a time if needed. Since Nova allows separate creationRules and updateRules for each field there should be three corresponding static methods on the FormRequest, and the non-static rules method can merge the rules for the rest of Laravel if needed.

Here’s the FormRequest class I’m using to provide validation rules for adding and editing users in an application I’m working on:


namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Rules\Postalcode;


class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
	    // Users authorized to make the request are:
	    // - users updating themselves.
	    // - staff
	    // - guests creating a new user.


        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {

    	$rules = self::ruleGetter($this);


	    if(empty($this->user())) {
		    $rules = array_merge_recursive($rules, self::creationRuleGetter($this));
	    } else {
		    $rules = array_merge_recursive($rules, self::updateRuleGetter($this));
	    }

	    return $rules;
    }

    public static function ruleGetter( $request, $rule = null ) {

	    $rules = [
		    'email' => [
			    'required',
			    'string',
			    'email',
			    'max:255'
		    ],
		    'nickname' => [
			    'required',
			    'max:255'
		    ],
		    'job_title' => [
		    	'sometimes',
		        'max:255'
		    ],
		    'organization' => [
			    'sometimes',
			    'max:255'

		    ],
		    'state' => [
		    	'sometimes',
			    'nullable',
			    'in:AL,IS,TO,FS,TA,TE,S'
			],
		    'postalcode' => [
			    'sometimes',
			    new Postalcode
		    ],

		    'phone' => [
		    	'max:255'
		    ]
	    ];

	    if(!empty($rule)) {
	    	if(isset($rules[$rule])) {
			    return $rules[$rule];
		    }
	    	return '';
	    }
	    return $rules;
    }

    public static function creationRuleGetter( $request, $rule = null ) {
	    $rules = [
	    	'email' => [
	    		'unique:users,email'
		    ],

	        'nickname' => [
	        	'required',
		        'unique:users,nickname'
	        ],

		    'password' => [
			    'sometimes',
			    'required',
			    'string',
			    'min:8',
			    'confirmed'
		    ],
	    ];


	    if(!empty($rule)) {
		    if(isset($rules[$rule])) {
			    return $rules[$rule];
		    }
		    return '';
	    }
	    return $rules;

    }

    public static function updateRuleGetter( $request, $rule = null ) {


    	$rules = [
    		'email' =>  [
		        Rule::unique('users', 'email')->ignore($request->user()->id)
		    ],

		    'nickname' => [
			    Rule::unique('users', 'nickname')->ignore($request->user()->id)
		    ],

		    'password' => [
			    'sometimes',
			    'nullable',
			    'string',
			    'min:8',
			    'confirmed'
		    ],
	    ];

	    if(!empty($rule)) {
		    if(isset($rules[$rule])) {
			    return $rules[$rule];
		    }
		    return '';
	    }
	    return $rules;
    }

}

Then, in the Nova resource the rules can be attached to individual fields like this:

<?php

namespace App\Nova;

use Laravel\Nova\Fields\ID;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Gravatar;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Password;
use Laravel\Nova\Panel;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Fields\Place;
use Laravel\Nova\Http\Requests\NovaRequest;


use App\Http\Requests\StoreUserRequest;

class User extends Resource
{

    /**
     * The model the resource corresponds to.
     *
     * @var string
     */
    public static $model = 'App\\User';


    // ....


   /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fields(Request $request)
    {

        return [
            ID::make()->sortable(),

            Gravatar::make(),

	        Text::make('Email')
		        ->sortable()
			    ->withMeta( [
			    	'extraAttributes' => [
			    		'type' => 'email'
				    ]
			    ])
		        ->rules(StoreUserRequest::ruleGetter($request, 'email'))
		        ->creationRules(StoreUserRequest::creationRuleGetter($request, 'email'))
		        ->updateRules('unique:users,email,{{resourceId}}'),

		    Password::make('Password')
		            ->onlyOnForms()
		            ->rules(StoreUserRequest::ruleGetter($request, 'password')),

		    Text::make('Name', 'name')
		        ->sortable()
		        ->rules(StoreUserRequest::ruleGetter($request, 'name')),

		    Text::make('Preferred Name')
		        ->rules(StoreUserRequest::ruleGetter($request, 'preferred_name'))
		        ->hideFromIndex(),

		    Text::make('Nickname')
		        ->rules(StoreUserRequest::ruleGetter($request, 'nickname'))
		        ->creationRules(StoreUserRequest::creationRuleGetter($request, 'nickname'))
		        ->updateRules('unique:users,nickname,{{resourceId}}')
		        ->hideFromIndex(),

		    Text::make('Job Title')
		        ->rules(StoreUserRequest::ruleGetter($request, 'job_title'))
		        ->hideFromIndex(),

		    Text::make('Company Name', 'organization')
		        ->rules(StoreUserRequest::ruleGetter($request, 'organization'))
		        ->hideFromIndex(),

		    Place::make('Address')
		        ->rules(StoreUserRequest::ruleGetter($request, 'address'))
			    ->countries(['US', 'CA'])
			    ->secondAddressLine('address2')
			    ->city('city')
			    ->state(['stateprov'])
			    ->postalCode('postalcode')
		        ->hideFromIndex(),

		    Text::make('Address Line 2', 'address2')
		        ->rules(StoreUserRequest::ruleGetter($request, 'address2'))
			    ->hideFromIndex(),

		    Text::make('City')
		        ->rules(StoreUserRequest::ruleGetter($request, 'city'))
		        ->hideFromIndex(),

		    Select::make('State or Province', 'stateprov')
			    ->options(get_stateprovs())
			    ->rules(StoreUserRequest::ruleGetter($request, 'stateprov'))
			    ->hideFromIndex(),

		    Text::make('ZIP or Postal Code', 'postalcode')
		        ->rules(StoreUserRequest::ruleGetter($request, 'postalcode'))
			    ->hideFromIndex(),

		    Text::make('Phone')
		        ->rules(StoreUserRequest::ruleGetter($request, 'phone'))
			    ->withMeta(['extraAttributes' => [
			    	'type' => 'tel'
			    ]])
		        ->hideFromIndex(),

        ];
    }

    // ....

}

Instead of writing out actual strings of rules for the Nova resource, the StoreUserRequest::ruleGetter method supplies the rule, allowing one canonical repository of rules for use with Users.

An Exception

There is an exception to the beauty: Enforcing unique values in a table, but ignoring a certain user ID, (for example when updating a user profile the user should not see a “This E-mail is already in use” error if the E-mail isn’t changed). Nova handles it like this:

// Simple and elegant 
Text::make('Email')
->rules('unique:users,email,{{resourceId}}')

Non-Nova Laravel handles it like this:

Rule::unique('users')->ignore($user->id, 'user_id')

But when declaring Nova fields a $user object, or the ID of the model that is being updated, is not available so this does not work. I have worked around the problem by writing out these few cases in the updateRules() method of the Nova field, as in this excerpt from above:

// Excerpt from full Nova Resource definition.
// Most Rules use one of the ruleGetter methods on StoreUserRequest
// but updateRules are just typed out to take advantage of Nova syntax.
Text::make('Email')
->rules(StoreUserRequest::ruleGetter($request, 'email'))
->creationRules(StoreUserRequest::creationRuleGetter($request, 'email'))
->updateRules('unique:users,email,{{resourceId}}'),

Other Possibilities

Ideally it would be possible to have the FormRequest object do the validation itself, without having to pass individual rules to Nova fields. Another possibility is to turn the Request that’s passed to a Resource’s fields method into a FormRequest, or to instantiate a completely new FormRequest object, and let the FormRequest object handle validation. Unfortunately I don’t know where we would do this, (the fields method doesn’t seem right), and I don’t know if the redirects and error messages generated by FormRequest validation would work properly with Nova. I would love to hear if someone can make this work.

Conclusion

I sincerely hope that Nova will soon allow automatic validation with FormRequest objects, but until that happens it is possible to keep code almost DRY and feed individual rules to Nova fields with ruleGetter methods on our FormRequests.

Preview Laravel Password Reset and Verification E-mails in a Browser

I’m building something on Laravel, using the built-in Auth with 5.7’s new E-mail Verification feature. Today’s task was to tweak the design of E-mail notifications a bit, so I wanted to preview them in a browser. I thought it would be easy, but the built-in E-mails use the Notification system, bypassing Mailable objects. Some Googling didn’t help much, but combining the Notifications documentation and this forum post got let me preview the messages. Hopefully this post can help someone else preview their messages much more quickly than I did today.

The concept is similar to the “Previewing Mailables” documentation: define a route to view your preview on, and return something that will render the message. The trick is that the MailMessage objects that the Notifications’ toMail() methods return don’t have a render() method like Mailable class does, so we need to find something else. Since the built-in Auth uses Markdown mail we can use the Markdown class. A Notifiable object, (the User object for the user who will get the notification), and a Notification are also needed.

For this to work you need to have the right Notifications published to your resources/views/vendor directory. This is done with the Artisan command php artisan vendor:publish --tag=laravel-notifications.

And set up the preview routes:


Route::get('/reset-notification', function () {

    // Get a user for demo purposes
    $user = App\User::find(1);

    // Make a Reset Notification object, (subclass of `Notification`)
    $resetNotification = new \Illuminate\Auth\Notifications\ResetPassword( 'some-random-string-this-will-be-much-longer-in-real-life' );

    // get the `MailMessage` object
    $mailMessage = $resetNotification->toMail($user);

    // get the `Markdown` object
    $markdown = new Illuminate\Mail\Markdown(view(), config('mail.markdown'));

    // Render the vendor.notifications.email view with data from the `MailMessage` object
    return $markdown->render('vendor.notifications.email', $mailMessage->toArray());
});


Route::get( '/verify-notification', function () {

    // Get a user for demo purposes
    $user = App\User::find(1);

    // Make a Verify Notification object, (subclass of `Notification`)
    $verifyNotification = new Illuminate\Auth\Notifications\VerifyEmail();

    // get the `MailMessage` object
    $mailMessage = $verifyNotification->toMail($user);

    // get the `Markdown` object
    $markdown = new Illuminate\Mail\Markdown(view(), config('mail.markdown'));

    // Render the vendor.notifications.email view with data from the `MailMessage` object
    return $markdown->render('vendor.notifications.email', $mailMessage->toArray());
});

Once the Notification is published and the routes created it is possible to preview the Password Reset and E-mail Verification E-mails in the browser.