Sending Webhook from Laravel to VtigerCRM

Sending Webhook from Laravel to VtigerCRM

The most common task that faces during development of various portals or online stores is integration with third-party systems such as CRM or ERP. And most often, to quickly update the data, you have to use Webhooks. For example, a client placed an order and the data on it must be quickly sent to CRM so that a deal is created in it, tasks are set for employees, an alert is sent, and so on. In this article, I will discuss a typical integration example, we will send simple data in such a way that it can be easily scaled in the future. We will also discuss the pitfalls that may arise during integration and how to solve them painlessly.

So, let's imagine that we have a Laravel 9 website with a simple feedback form. After the user fills all the fields, clicks the submit button, we need to save the data to the website database and send it to Vtiger CRM by creating a lead there. Looking ahead, I will immediately say that we will solve the problem in a slightly non-standard way. Let's try to imagine that we are not only creating a lead, but launching some kind of complex business process. In other words, the standard API that Vtiger CRM provides us will not work. You will need to create some kind of custom webhook.

Let's see what we have in Laravel. I will not apply the layout of the blade files itself, if necessary, you can see the entire code on my github - https://github.com/semelyanov86/laravel-test/commit/9dedaf4629aab1e3dcaf5e2503cbcb36b0de2f2e.

First, our lead base structure looks like this:

 

return new class extends Migration {

    public function up()

    {

        Schema::create('leads', function (Blueprint $table) {

            $table->id();

 

            $table->string('first_name', 190);

            $table->string('last_name', 190);

            $table->string('email', 100);

            $table->string('phone', 100);

            $table->text('description')->nullable();

 

            $table->timestamps();

        });

    }

 

    public function down()

    {

        Schema::dropIfExists('leads');

    }

};

 

I connected Laravel with RabbitMQ, also installed some common composer packages lorisleiva/laravel-actions and spatie/data-transfer-object.

This is Lead model:

 

namespace App\Models;

 

use Illuminate\Database\Eloquent\Model;

 

class Lead extends Model

{

    protected $fillable = [

        'first_name',

        'last_name',

        'email',

        'phone',

        'description',

    ];

}

 

So, we have a list of fields, let's create everything necessary in Vtiger to send data after sending a form.

Vtiger provides us with a handy tool called ShortURL, which allows us to quickly create a link for receiving and processing data from third-party sites. To create and register it, let's write a script. I like to create a special scripts folder and store all of them in it.

Let's create a file scripts/create_leads_webhook.php with following content:

 

<?php

chdir('../');

require_once 'include/utils/utils.php';

require_once 'include/utils/VtlibUtils.php';

require_once 'modules/Vtiger/helpers/ShortURL.php';

global $adb;

$adb = PearDatabase::getInstance();

$options = array(

    'handler_path' => 'modules/Leads/handlers/LeadCreateTaskHandler.php',

    'handler_class' => 'Leads_CreateTask_Handler',

    'handler_function' => 'createLead',

    'handler_data' => array()

);

$trackURL = Vtiger_ShortURL_Helper::generateURL($options);

var_dump($trackURL);

 

Run this file in browser and you will see our link. In my case, this link will be following: http://vtiger.test:8000/shorturl.php?id=6303d18dec9038.15461100. Just save it, we will need this data in the future.

Now we need to write a handler that will create leads. Let's create it in the folder with the modules/Leads/handlers/LeadCreateTaskHandler.php :

 

 

class Leads_CreateTask_Handler

{

 

    public function createLead($data): void

    {

        global $current_user;

        $current_user->id = 1;

        $dataRequest = json_decode(file_get_contents('php://input'), true);

        $response = new Vtiger_Response();

        $response->setEmitType(Vtiger_Response::$EMIT_JSON);

        $leadModel = Vtiger_Record_Model::getCleanInstance('Leads');

        $leadModel->set('mode', 'create');

        $leadModel->set('lastname', $dataRequest['last_name']);

        $leadModel->set('firstname', $dataRequest['first_name']);

        $leadModel->set('email', $dataRequest['email']);

        $leadModel->set('description', $dataRequest['description']);

        $leadModel->set('phone', $dataRequest['phone']);

        $leadModel->set('assigned_user_id', 1);

        $leadModel->set('designation', $dataRequest['portal_id']);

        $leadModel->save();

 

        $response->setResult(['success' => true]);

 

        $response->emit();

    }

}

 

In our example, we simply create a lead, fill the model with the data that comes to us from Laravel and save it. Of course, we missed one important thing - find the entry before that and if it is in the system, update it and not create a new one. To protect against duplicates. But in this article we consider only the general principle. Implementation details are up to you.

Note, that there are one service field, which I called designation. In it, I will store the lead id that we received from Laravel, so in the future we can search for duplicates by this field and link leads between systems.

Now let's move on to the most essential part - sending data from Laravel.

Before we lose the generated url, let's save it in the configuration file. To do this, add a new key to the config/services.php file:

 

'lead_webhook' => env('LEAD_WEBHOOK'),

 

And in our .env file store the value:

 

LEAD_WEBHOOK=http://vtiger.test:8000/shorturl.php?id=6303d18dec9038.15461100

 

So, let's go to the actual process of saving and sending the webhook.

First, we need to remember to validate the form itself. Create Request in app/Http/Requests/StoreLeadRequest.php file

 

<?php

declare(strict_types=1);

 

namespace App\Http\Requests;

 

use Illuminate\Foundation\Http\FormRequest;

 

final class StoreLeadRequest extends FormRequest

{

    public function rules(): array

    {

        return [

            'first_name' => ['required', 'min:3', 'max:190'],

            'last_name' => ['required', 'min:3', 'max:190'],

            'email' => ['required', 'email'],

            'phone' => ['nullable', 'alpha_num'],

            'description' => ['nullable', 'string']

        ];

    }

 

    public function authorize(): bool

    {

        return true;

    }

}

 

In order to properly process the Request, we need to create a DTO, which we then transfer to the Action from the controller. Let's do that. Create file app/Transfers/LeadTransfer.php :

 

<?php

 

declare(strict_types=1);

 

namespace App\Transfers;

 

final class LeadTransfer extends \Spatie\DataTransferObject\DataTransferObject

{

    public ?int $id;

 

    public string $first_name;

 

    public string $last_name;

 

    public string $email;

 

    public string $description;

 

    public string $phone;

}

 

Now let's write an Action, in which we will implement a simple save process (file app/Actions/StoreLeadAction.php):

 

<?php

 

declare(strict_types=1);

 

namespace App\Actions;

 

use App\Jobs\VtigerWebhookJob;

use App\Models\Lead;

use App\Transfers\LeadTransfer;

use Lorisleiva\Actions\Concerns\AsAction;

 

final class StoreLeadAction

{

    use AsAction;

 

    public function handle(LeadTransfer $transfer): LeadTransfer

    {

        $leadModel = Lead::create($transfer->toArray());

        $transfer->id = $leadModel->id;

        VtigerWebhookJob::dispatch($transfer);

        return $transfer;

    }

}

 

This class has a simple implementation procedure. We convert the DTO that came to us into an array, create a lead, write back the received ID from the database to the DTO and call the queue, which we will write a little later.

Yes, in our case, we need to work with the queues. We never know what answer will come to us from CRM and whether the communication channel between servers is stable. Therefore, it is significant to organize integration between systems in an asynchronous mode.

Now we connect everything together in the controller itself (app/Http/Controllers/LeadsController.php file):

 

<?php

declare(strict_types=1);

 

namespace App\Http\Controllers;

 

use App\Actions\StoreLeadAction;

use App\Http\Requests\StoreLeadRequest;

use App\Transfers\LeadTransfer;

 

class LeadsController extends Controller

{

    public function create(): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory

    {

        return view('leads.create');

    }

 

    public function store(StoreLeadRequest $request, StoreLeadAction $action): \Illuminate\Http\RedirectResponse

    {

        $dto = new LeadTransfer($request->validated());

        $result = $action->handle($dto);

        return redirect()->back()->with('message', 'Lead created with id: ' . $result->id);

    }

}

 

Now it remains only to create the Job itself, which will be responsible for sending the webhook. The complete file (app/Jobs/VtigerWebhookJob.php) is attached below, I will explain each action line by line:

 

<?php

declare(strict_types=1);

 

namespace App\Jobs;

 

use App\Transfers\LeadTransfer;

use Illuminate\Bus\Queueable;

use Illuminate\Contracts\Queue\ShouldQueue;

use Illuminate\Foundation\Bus\Dispatchable;

use Illuminate\Queue\InteractsWithQueue;

use Illuminate\Queue\SerializesModels;

use Illuminate\Support\Facades\Http;

use Illuminate\Support\Facades\Log;

 

final class VtigerWebhookJob implements ShouldQueue

{

    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

 

    public int $tries = 0;

 

    public function __construct(

        protected LeadTransfer $leadTransfer

    )

    {

    }

 

    public function retryUntil(): \Illuminate\Support\Carbon

    {

        return now()->addDay();

    }

 

    public function handle(): void

    {

        $data = $this->leadTransfer->toArray();

        $data['portal_id'] = $this->leadTransfer->id;

        unset($data['id']);

        $response = Http::timeout(5)->post(config('services.lead_webhook'), $data);

        if ($response->failed()) {

            $this->release(

                now()->addMinutes(15 * $this->attempts())

            );

        }

    }

 

    public function failed(\Exception $e): void

    {

        Log::error('Failed to send Lead to Vtiger with id: ' . $this->leadTransfer->id . ' Error:' . $e->getMessage());

    }

}

 

Inside VtigerWebhookJob.php, we are going to use Laravel's build-in HTTP client to send requests to the Vtiger URL:

 

Http::timeout(5)->post(config('services.lead_webhook'), $data)

 

I configured a 5-second timeout on the HTTP client. This will prevent the job from getting stuck waiting indefinitely for a response. Always set a reasonable timeout when making requests from your code.

If we receive a response status that is not 2xx, we want to retry the job several times before marking it as failed. But instead retrying the job for a specific number of times, we want to retry it for a maximum of 24 hours.

First let's check an error response and release the job back to the queue with an exponential backoff:

 

        if ($response->failed()) {

            $this->release(

                now()->addMinutes(15 * $this->attempts())

            );

        }

 

Method attempts return the number of times the job has been attempted. If it's the first run, attempts will return 1.

In our case, at the first unsuccessful start, we will run the task the next time in 15 minutes. At the second - in 30 minutes. At the third - through 45. And so on.

But we agreed that it is necessary to send data only within 24 hours, right? We could do a complex calculation in minutes and repeat only a certain number of times... But there is a much easier way:

 

    public function retryUntil(): \Illuminate\Support\Carbon

    {

        return now()->addDay();

    }

Adding a retryUntil public method to our job class instructs Laravel to set an expiration date for the job. In this case, the job will expire after 24 hours. The expiration is calculated be calling the retryUntil method when the job is first dispatched, If you release the job back, the expiration will not be re-calculated.

But remember, only one attempt is allowed by default when you start a worker. In our case, we want this job to retry for an unlimited number of times since we already have an expiration in place. To do this, we need to set the $tries public property to 0.

 

    public int $tries = 0;

 

Now Laravel will not fail this job based on the number of attempts.

If the HTTP request for the webhook kept failing for more than 24 hours Laravel will mark the job as failed. In that case, we want to notify the administrator so that they can react. If you have a good logging system it is enough to have a simple function.

 

    public function failed(\Exception $e): void

    {

        Log::error('Failed to send Lead to Vtiger with id: ' . $this->leadTransfer->id . ' Error:' . $e->getMessage());

    }

 

When a job exceeds the assigned number of attempts or reaches the expiration time, the worker is going to call failed method and pass a MaxAttemptsExceededException. In this method we send the data in log, from there we send info to Logging system, which notify an administrator.

 

Thus, we have created a basic and scalable system for sending leads from Laravel to Vtiger. On the CRM side, we used a custom webhook rather than the standard API. And on the Laravel side, we send data using a queue with periodic repetition in case of failure. If the submission does not occur within 24 hours, we will notify the administrator.

 

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235