PHP and Symfony

This tutorial will show you how to use the Lokalise PHP SDK to create webhooks, listen to webhook events in third-party apps, and handle incoming notifications.

📘

In this tutorial you'll learn how to...

  • Work with Lokalise API tokens
  • Create webhooks with the Lokalise API
  • Listen to webhooks events
  • Handle incoming notifications

You can find the source code on GitHub.

Prerequisites

This guide assumes that you have a Lokalise project (if not, learn how to create your Lokalise project here). In this tutorial, we are going to upload English translations. Therefore, make sure that your project has the English language added with the “en” ISO code.

If you want to follow this guide locally on your computer, you need to have the following software installed:

What we are going to build

We are going to create a simple application that will allow users to register webhooks in their Lokalise projects. The app will also listen to incoming notifications generated by these webhooks and react to them by sending API requests.

Preparing a new Symfony app

Create a new Symfony app by running the following command:

symfony new lokalise-webhooks --webapp

This will create the skeleton app in the lokalise-webhooks folder for you.

Now go into that folder and set the Symfony app to use the Lokalise PHP SDK:

cd lokalise-webhooks
composer require lokalise/php-lokalise-api

Getting the API token

You should obtain an existing API token or generate a new one. Learn how to get a Lokalise API token. Store it inside the .env file in the following way:

LOKALISE_TOKEN=123def456

Open config/services.yaml and add token as a parameter:

parameters:
    lokalise.api_token: '%env(LOKALISE_TOKEN)%'

Running your app

In order to follow this tutorial, you will need to deploy your app to some hosting. Also, you can use ngrok:

  • Run the following command: symfony server:start
  • Follow the ngrok getting started guide
  • In your terminal, navigate to the folder where you unzipped ngrok
  • Run ./ngrok config add-authtoken <token>
  • Finally, run ./ngrok http 800
ngrok                                                                                                                                                                                                                                                                                                                                                     (Ctrl+C to quit)

Session Status                online
Account                       User Name (Plan: Free)
Version                       3.0.3
Region                        Europe (eu)
Latency                       calculating...
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://50e1-90-133-221-110.eu.ngrok.io -> http://localhost:8000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

📘

Your app must be publicly accessible

If your Symfony app is not publicly accessible, it won't be possible to register a Lokalise webhook. That's because Lokalise sends a special "ping" request to the provided URL and expects to receive a 2xx status code. If the URL is not accessible, the webhook won't be created.

Adding template

Now let's add a new template to the templates/web_hooks/index.html.twig file:

{% for message in app.flashes('notice') %}
    <div><strong>{{ message }}</strong></div>
{% endfor %}
<br />

<form method="post" action="{{ path('app_hooks_register')}}">
    <label>Enter Project ID: <input type="text" name="projectId" /></label><br /><br />
    <input type="submit" value="Register webhook" />
</form>

We will use this form to enter a project ID and register a new webhook.

Registering a new webhook

Tweak the src/Controllers/WebHooksController.php file to create a new webhook for the chosen project:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;

class WebHooksController extends AbstractController
{
    #[Route('/', name: 'app_hooks_home')]
    public function index(SessionInterface $session): Response
    {
        return $this->render('web_hooks/index.html.twig');
    }

    #[Route('/registerHook', name: 'app_hooks_register', methods: ['POST'])]
    public function registerWebhook(SessionInterface $session, Request $request): Response
    {
        $projectId = $request->request->get('projectId', null);

        if (empty($projectId)) {
            $this->addFlash('notice', 'Please enter a project id');
            return $this->redirect($this->generateUrl('app_hooks_home'));
        }

        $client = new \Lokalise\LokaliseApiClient($this->getParameter('lokalise.api_token'));

        $client->webhooks->create(
            $projectId,
            [
                'url' => 'https://50e1-90-133-221-110.eu.ngrok.io/triggerHook',
                'events' => [
                    'project.key.added',
                ],
            ]
        );


        $this->addFlash('notice', 'Hook registered');

        return $this->redirect($this->generateUrl('app_hooks_home'));
    }

This webhook will listen to the project.key.added event. Notifications will be sent to the /triggerHook route.

Responding to notifications

Now let's tweak the src/Controllers/WebHooksController.php file again to add a new /triggerHook route listening to all the incoming events:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;

class WebHooksController extends AbstractController
{
    #[Route('/triggerHook', name: 'app_hooks_trigger', methods: ['POST'])]
    public function triggerHook(Request $request): Response
    {
        $data = json_decode($request->getContent(), true, 5, JSON_THROW_ON_ERROR);
        $event = $data['event'] ?? null;

        if ($event === 'project.key.added') {
            $keyId = $data['key']['id'] ?? null;
            $projectId = $data['project']['id'] ?? null;

            if ($projectId && $keyId) {
                $client = new \Lokalise\LokaliseApiClient($this->getParameter('lokalise.api_token'));
                $client->comments->create($projectId, $keyId, [
                    'comments' => [
                        [
                            'comment' => '@Bob please review this new key ',
                        ],
                    ]
                ]);
                $client->keys->update($projectId, $keyId, [
                    'is_hidden' => true
                ]);
            }
        }

        return new Response('ok');
    }
}

Webhook data is sent in JSON format. We make sure that the received notification is not a ping and has a proper event name. Then, we just add a key comment and update the key using event's data.

You can introduce an additional level of security by checking the request headers. Specifically, each reqest with contain a special "secret" (that you can customize when creating a webhook) as well as the Lokalise project ID and the webhook ID. Please find more information in the corresponding document.

📘

A notice on notification handlers

Please note that your event handlers must respond to POST requests and return 2xx status codes. Otherwise, Lokalise will consider the webhook notification to be unsuccessful and will try to re-send failed notifications multiple times.

Testing it out

Now everything is ready! Open your app and enter a project ID to register a new webhook. Open Lokalise, proceed to your project and click "Apps" in the top menu. Find the "Webhooks" app, click on it, and then press "Manage". You'll see that a new webhook was registered for you:

Note the "X-Secret header" hidden value. You can use this value inside your app and compare it with the one sent to the notify route for extra protection (thus filtering out malicious requests).

Now return to the Lokalise project editor and create a new translation key. Reload the page and make sure the key was hidden:

Click on the "Comments" button (the first button on the screen above) and make sure the comment is displayed properly:

That's it, great job!

Webhooks and bulk actions

Webhooks support Lokalise bulk actions as well as the find/replace operation. You can find more info the Webhooks article but let's see how to listen to such events.

First of all, it's important to enable the proper events in the webhooks configuration. For instance, if you'd like to monitor find/replace operation as well as applying pseudolocalization using bulk actions, you should tick the translation updated event:

Please keep in mind that as long as the bulk actions may involve multiple keys, the event payload will contain data from 1 and up to 300 keys. If more than 300 keys were involved in the operation, you'll receive multiple events.

Also note that in case of bulk actions, the event name will be pluralized: project.translations.updated, not project.translation.updated. For example, if you would like to display information about all the updated translations as well as project and user name, you can use the following code snippet:

#[Route('/triggerHook', name: 'app_hooks_trigger', methods: ['POST'])]
public function triggerHook(Request $request): Response
{
    $data = json_decode($request->getContent(), true, 5, JSON_THROW_ON_ERROR);
    $event = $data['event'] ?? null;

    if ($event === 'project.translations.updated') {
        echo "Project name: {$data['project']['name']}";
        echo "User name: {$data['user']['full_name']}";
        
        foreach ($data['translations'] as $translation) {
            echo "Translation ID: {$translation['id']}";
            echo "Translation value: {$translation['value']}";
            echo "Language: {$translation['language']['name']}";
            echo "Key: {$translation['key']['name']}";
        }
    }

    return new Response('ok');
}

So, 'translations' contain an array with all the translations data, including the language and key details.