Before diving into the manual, you can clone a ready-made boilerplate implementation for Zoho CRM integration for Laravel and Symfony.
https://github.com/blowstack/zoho-crm-laravel.git
https://github.com/blowstack/zoho-crm-symfony.git
To effectively utilize this article, you should have already implemented a service that manages the authentication process. I discuss how to accomplish this in Authenticate Zoho CRM in PHP article, including code snippets at the end. Each Zoho API call requires a configuration and an access token; therefore, utilizing this service simplifies the process for you. Moreover, it facilitates the implementation of numerous solutions within this service (specifically, ZohoCRM).
Creating a Form and Controller for Lead Generation
For lead generation purposes, I created a contact form to collect basic data. Following this, you'll find a controller and a view. These components are implemented separately in both Symfony and Laravel, a common approach throughout this post. The same code is consistently applied within these frameworks, specifically within the ZohoCRM service.
Symfony Implementation
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ContactFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->add('company', TextType::class ,['required' => false])
->add('email', EmailType::class)
->add('phone', TextType::class)
->add('content', TextareaType::class)
->add('save', SubmitType::class)
;
}
}
Next, create a controller to manage the form and its view:
<?php
namespace App\Controller;
use App\Service\ZohoCRM;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Form\ContactFormType;
class ContactController extends AbstractController
{
/**
* @Route("/contact", name="contact")
* @param Request $request
* @param ZohoCRM $zohoCRM
* @return \Symfony\Component\HttpFoundation\Response
*/
public function index(Request $request, ZohoCRM $zohoCRM)
{
$ContactForm = $this->createForm(ContactFormType::class);
$ContactForm->handleRequest($request);
if($ContactForm->isSubmitted() &amp;amp; $ContactForm->isValid()) {
$data = $ContactForm->getData();
$zohoCRM->createLead(
$data['firstName'],
$data['lastName'],
$data['email'],
$data['content'],
$data['company'],
$data['phone']
);
}
return $this->render('contact/index.html.twig', [
'Form' => $ContactForm->createView(),
]);
}
}
Template for the contact form (templates/contact/index.html.twig
):
// templates/contact/index.html
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-10 col-lg-4 text-center text-white">
<h1 class="mt-5">Contact</h1>
{{ form_start(Form) }}
<div class="form-group">
{{ form_label(Form.firstName) }}
{{ form_widget(Form.firstName, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_label(Form.lastName) }}
{{ form_widget(Form.lastName, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_label(Form.company) }}
{{ form_widget(Form.company, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_label(Form.email) }}
{{ form_widget(Form.email, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_label(Form.phone) }}
{{ form_widget(Form.phone, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_label(Form.content) }}
{{ form_widget(Form.content, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_widget(Form.save, {'attr': {'class': 'btn btn-primary'}, 'label': 'Send message'}) }}
</div>
{{ form_end(Form) }}
</div>
</div>
</div>
{% endblock %}
Create a Basic Lead
There are eight basic and most popular fields a lead can consist of, with the first name and last name being obligatory. Below is a simple implementation that covers these eight basic fields to be passed to the CRM. To accomplish this, let's add a new method in the service.
ZohoCRM Service (src/Service/ZohoCRM
)
...
public function createLead($firstName, $lastName, $email, $description = null, $company = null, $phone = null)
{
$RecordLead = ZCRMRecord::getInstance("Leads",null);
$RecordLead->setFieldValue('First_Name', $firstName);
$RecordLead->setFieldValue('Last_Name', $lastName);
$RecordLead->setFieldValue('Email', $email);
$RecordLead->setFieldValue('Company', $company);
$RecordLead->setFieldValue('Phone', $phone);
$RecordLead->setFieldValue('Description', $description);
$RecordLead->setFieldValue('Lead_Source', 'contact form');
$Leads = ZCRMModule::getInstance('Leads');
$arrayOfLeads = [];
array_push($arrayOfLeads, $RecordLead);
try {
$response = $Leads->createRecords($arrayOfLeads);
$result = $response->getData()[0]->getEntityId();
}
catch (Exception $exception) {
// this is not proper way to handle exceptions on the production env
return $exception;
}
}
..
}
Get all possible lead fields
Wondering about the other fields beyond the basic ones? Discovering all the fields that can be passed to Zoho CRM is straightforward. Simply query the CRM with your first inserted lead. To extend the utility, we'll introduce a function that allows searching for records from any module, not limited to leads.
ZohoCRM Service
In your ZohoCRM
service file located at src/Service/ZohoCRM.php
, add the following function to perform searches:
...
function getRecords($moduleName, $email) {
$Module = ZCRMModule::getInstance($moduleName);
return $Module->searchRecordsByEmail($email);
}
...
This function leverages email for searching, but Zoho CRM provides multiple methods for record search, including:
searchRecordsByPhone($phone)
searchRecordsByWord($word)
searchRecordsByCriteria($criteria)
The searchRecordsByCriteria
method is particularly flexible, allowing searches by any field (e.g., last name with ['Last_Name' => $lastName]
) or combinations thereof.
To review the search response, implement a new ZohoController
and a corresponding Twig template. Pass the lead's email address via the URL to initiate the search.
Symfony Implementation
Create a controller in src/Controller/admin/ZohoController.php
:
<?php
namespace App\Controller\admin;
use App\Service\ZohoCRM;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class ZohoController extends AbstractController
{
/**
* @Route("/admin/zoho/lead/{email}", name="admin_zoho_lead")
* @param ZohoCRM $zohoCRM
* @return \Symfony\Component\HttpFoundation\Response
*/
public function index(ZohoCRM $zohoCRM, $email)
{
$lead = $zohoCRM->getRecords('Leads', $email);
return $this->render('admin/lead.html.twig', [
'lead' => $lead,
]);
}
}
And the Twig template templates/admin/lead.html.twig
:
// templates/admin/lead.html.twig
{% extends 'base.html.twig' %}
{% block title %}Lead Details{% endblock %}
{% block body %}
<h2>Lead Information</h2>
{{ dump(lead) }}
{% endblock %}
This function and template setup will display the structure of the response from Zoho CRM, showing all available fields in a JSON format, which can be invaluable for understanding the data model and planning further integrations.
Handling the Response
The response object, BulkAPIResponse
, encapsulates the data in a format where you can access all fields that can be sent to Zoho CRM. Note that this method can return multiple records if the search criteria are met. The structure includes a data
array with detailed fields for each record.
zcrmsdk\crm\api\response\BulkAPIResponse {#413 ▼
-bulkData: array:1 [▶]
-status: null
-info: zcrmsdk\crm\api\response\ResponseInfo {#412 ▶}
-bulkEntitiesResponse: []
-httpStatusCode: 200
-responseJSON: array:2 [▼
"data" => array:1 [▼
0 => array:55 [▼
"Owner" => array:3 [▶]
"Company" => "BlowStack"
"Email" => "b.stack@blowstack.com"
"$currency_symbol" => "$"
"Visitor_Score" => null
"Last_Activity_Time" => null
"Industry" => null
"$state" => "save"
"$converted" => false
"$process_flow" => false
"Street" => null
"Zip_Code" => null
"id" => "3652704000000955001"
"$approved" => true
"$approval" => array:4 [▶]
"First_Visited_URL" => null
"Days_Visited" => null
"Created_Time" => "2020-07-15T21:22:26+02:00"
"$editable" => true
"City" => null
"No_of_Employees" => null
"State" => null
"Country" => null
"Last_Visited_Time" => null
"Created_By" => array:3 [▶]
"Annual_Revenue" => null
"Secondary_Email" => null
"Description" => "This is a message you should read."
"Number_Of_Chats" => null
"Rating" => null
"$review_process" => array:3 [▶]
"Website" => null
"Twitter" => null
"Average_Time_Spent_Minutes" => null
"Salutation" => null
"First_Name" => "Blow"
"Full_Name" => "Blow Stack"
"Lead_Status" => null
"Record_Image" => null
"Modified_By" => array:3 [▶]
"$review" => null
"Skype_ID" => null
"Phone" => "+48483232323"
"Email_Opt_Out" => false
"Designation" => null
"Modified_Time" => "2020-07-15T21:22:26+02:00"
"$converted_detail" => []
"Mobile" => null
"$orchestration" => null
"First_Visited_Time" => null
"Last_Name" => "Stack"
"$in_merge" => false
"Referrer" => null
"Lead_Source" => "contact form"
"Fax" => null
]
]
"info" => array:4 [▶]
]
To add or update fields for a lead, use the following snippet within your ZohoCRM service methods, specifying the field name and value according to Zoho's notation (capitalizing the first letter of each word and using underscores between words)
...
$RecordLead->setFieldValue('Field_Name', $value);
...
This approach allows for flexible and dynamic interaction with the Zoho CRM API, enabling the creation and update of lead records with any of the available data fields.
Uploading Attachments
To upload an attachment, such as an image, you must first have a lead or deal created in Zoho CRM; this cannot be accomplished in a single API call. We'll adjust the contact form to accommodate file uploads.
Symfony Implementation for the Contact Form
// src/Form/ContactFormType.php
...
->add('attachment', FileType::class, ['required' => false])
...
Modify your controller to handle the file upload. If you're using Symfony version 5 or higher, inject KernelInterface
to access the getProjectRoot()
method. For older versions, retrieve the project directory using $this->get('kernel')->getProjectDir()
.
// src/Controller/ContactController.php
public function index(Request $request, ZohoCRM $zohoCRM, KernelInterface $kernelInterface) {
...
if($ContactForm->isSubmitted() &amp;amp; $ContactForm->isValid()) {
...
if($data['attachment']) {
$directory = $kernelInterface->getProjectDir() . '/tmp/files';
$fileName = pathinfo($data['attachment']->getClientOriginalName(), PATHINFO_FILENAME) .'.' . $attachment->guessExtension();
$attachment->move($directory, $fileName );
$attachment_path = $directory . '/' . $fileName;
}
...
$zohoCRM->createLead(
...
$attachment_path ?? null
);
This modification saves the uploaded file in a temporary folder and passes its path to the Zoho service.
Adjusting the ZohoCRM Service for Attachments
Now, let's ensure the ZohoCRM service can handle the attachment:
// src/Service/ZohoCRM.php
...
public function createLead($firstName, $lastName, $email = null, $description = null, $company = null, $phone = null, $attachment = null) {
...
try {
$response = $Leads->createRecords($arrayOfLeads);
$leadId = $response->getData()[0]->getEntityId();
if ($attachment) {
$RecordLead = ZCRMRecord::getInstance('Leads', $leadId);
$RecordLead->uploadAttachment($attachment);
}
}
...
This update adds support for uploading an attachment after the lead has been created.
Testing Complete Basic Lead Integration Using the Contact Form
With these changes, your Symfony application can now handle file attachments in forms and upload these files to Zoho CRM as attachments to leads.
Creating a Form and Controller for Deal Generation
Following a similar structure, you can create forms and controllers for other purposes, such as deal generation. This approach demonstrates the flexibility and power of integrating Symfony applications with external APIs like Zoho CRM for comprehensive CRM solutions.
Symfony Implementation for Deal Generation
Here's how you can set up a basic form and controller for generating deals in Symfony, similar to the lead generation process described above.
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class OrderFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('course', ChoiceType::class, [
'choices' => [
'Symfony' => 'Symfony',
'Laravel' => 'Laravel',
'Phalcon' => 'Phalcon',
]
])
;
}
}
And the corresponding controller:
<?php
namespace App\Controller;
use App\Form\OrderFormType;
use App\Service\ZohoCRM;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class OrderController extends AbstractController
{
/**
* @Route("/order", name="order")
* @param Request $request
* @param ZohoCRM $zohoCRM
* @return \Symfony\Component\HttpFoundation\Response
*/
public function index(Request $request, ZohoCRM $zohoCRM)
{
$OrderForm = $this->createForm(OrderFormType::class);
$OrderForm->handleRequest($request);
if ($OrderForm->isSubmitted() &amp;amp;&amp;amp; $OrderForm->isValid())
{
$data = $OrderForm->getData();
$dealName = $data['course'];
$zohoCRM->createDeal($dealName);
}
return $this->render('order/index.html.twig', [
'Form' => $OrderForm->createView()
]);
}
}
This setup allows for the generation of deals based on the selected course, demonstrating the versatility of Symfony forms for various business processes.
// templates/order/index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-10 col-lg-4 text-center text-white">
<h1 class="mt-5">Order</h1>
{{ form_start(Form) }}
<div class="form-group">
{{ form_label(Form.course) }}
{{ form_widget(Form.course, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_widget(Form.save, {'attr': {'class': 'btn btn-primary'}, 'label': 'Send message'}) }}
</div>
{{ form_end(Form) }}
</div>
</div>
</div>
{% endblock %}
Creating a Basic Deal in Zoho CRM
There is only one mandatory field. You have to pass a deal name to successfully create a new deal. You can obtain the rest of the possible fields similarly to the way presented for lead generation. There are also two fields which have to be passed differently and I will cover them later. Those fields are company name and account.
ZohoCRM Service
// src/Service/ZohoCRM.php
public function createDeal($dealName) {
$RecordDeal = ZCRMRecord::getInstance("Deals",null);
$RecordDeal->setFieldValue('Deal_Name', $dealName);
$Deals = ZCRMModule::getInstance('Deals');
$arrayOfDeals = [];
array_push($arrayOfDeals, $RecordDeal);
try {
$response = $Deals->createRecords($arrayOfDeals);
$dealId = $response->getData()[0]->getEntityId();
}
catch (Exception $exception) {
// Proper exception handling recommended for production environments
return $exception;
}
}
Creating a Contact and an Account
Before creating a lead, you must first create a contact or account and pass their respective IDs to the deal. Since an account can have many contacts, it's advisable to create the account first and then pass its ID to the contact. These IDs can subsequently be passed to the lead. You can consider an account as a company and contacts as its employees.
Adjusting the Order Form in Symfony
Modify the OrderFormType
to include fields for first name, last name, company, and email:
// src/Form/OrderFormType.php
...
class OrderFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->add('company', TextType::class)
->add('email', EmailType::class ,['required' => false])
...
}
}
Next, adjust the OrderController
to handle these new fields:
// src/Controller/OrderController.php
...
$data = $OrderForm->getData();
$dealName = $data['course'];
$firstName = $data['firstName'];
$lastName = $data['lastName'];
$email = $data['email'];
$account = $data['company'];
$zohoCRM->createDeal(
$dealName,
$firstName,
$lastName,
$email,
$account
);
...
Extending the ZohoCRM Service
To enhance the functionality of our ZohoCRM service, we will introduce four new methods. These methods are designed to streamline the creation and association of accounts and contacts with deals. Initially, focus on making these methods functional; optimization and refactoring can be addressed subsequently.
Service Implementation
Located at src/Service/ZohoCRM.php
, add the following methods:
Create Account
public function createAccount($companyName) {
if (!empty($companyName)) {
$recordAccount = ZCRMRecord::getInstance("Accounts", null);
$recordAccount->setFieldValue('Account_Name', $companyName);
$array = [$recordAccount];
$accountsModule = ZCRMModule::getInstance('Accounts');
$response = $accountsModule->createRecords($array);
return $response->getData()[0]->getEntityId();
}
return null;
}
Find Contact ID by Email
public function findContactId($email) {
$contactsModule = ZCRMModule::getInstance('Contacts');
try {
$result = $contactsModule->searchRecordsByEmail($email);
if (!empty($result->getData())) {
return $result->getData()[0]->getEntityId();
}
} catch (\Exception $exception) {
return null;
}
}
Create Contact
public function createContact($firstName, $lastName, $email, $accountId = null) {
$recordContact = ZCRMRecord::getInstance("Contacts", null);
$recordContact->setFieldValue('First_Name', $firstName);
$recordContact->setFieldValue('Last_Name', $lastName);
$recordContact->setFieldValue('Email', $email);
if ($accountId) {
$recordContact->setFieldValue('Account_Id', $accountId);
}
$array = [$recordContact];
$contactsModule = ZCRMModule::getInstance('Contacts');
$response = $contactsModule->createRecords($array);
return $response->getData()[0]->getEntityId();
}
Enhancing the CreateDeal Method
Adjust the createDeal
method to include the logic for account and contact creation/association:
public function createDeal($dealName, $firstName, $lastName, $email, $company = null) {
$accountId = $company ? $this->createAccount($company) : null;
$contactId = $this->createContact($firstName, $lastName, $email, $accountId);
$recordDeal = ZCRMRecord::getInstance("Deals", null);
$recordDeal->setFieldValue('Deal_Name', $dealName);
if ($contactId) $recordDeal->setFieldValue('Contact_Id', $contactId);
if ($accountId) $recordDeal->setFieldValue('Account_Id', $accountId);
$dealsModule = ZCRMModule::getInstance('Deals');
$arrayOfDeals = [$recordDeal];
try {
$response = $dealsModule->createRecords($arrayOfDeals);
return $response->getData()[0]->getEntityId();
} catch (Exception $exception) {
// Proper exception handling recommended for production environments
return $exception;
}
}
Testing the Integration
With these enhancements, your ZohoCRM service is now equipped to handle the creation of deals with associated accounts and contacts. Test the integration by submitting data through the order form, ensuring that the deal, along with its related account and contact, are successfully created in Zoho CRM.
This approach ensures that the service is not only functional but also ready for future optimization, providing a solid foundation for integrating Symfony applications with Zoho CRM's capabilities.