PHP

★ Asserting valid and invalid requests in Laravel

[AdSense-A]

Testing a Laravel project is one of the most pleasant experiences I've ever had: there's a clean testing API, a very powerful layer added on top of testing frameworks; all while keeping the simplicity and eloquence you'd expect from a Laravel project.

Here's a great example of Laravel's powerful simplicity. Recently, an improved way to test whether a request has validation errors or not was added. You can now use assertValid and assertInvalid instead of assertSessionHasErrors or assertJsonValidationErrors:

public function test_post_validation()
{
$this
->post(
action(UpdatePostController::class),
[
'title' => null,
'date' => '2021-01-01',
'author' => 'Brent',
'body' => null,
],
)
->assertValid(['date', 'author'])
->assertInvalid(['title', 'body']);
}

It's even possible to check for specific validation errors:

public function test_post_validation()
{
$this
->post(/* … */)
->assertInvalid([
'title' => 'required',
'body' => 'required',
]);
}

It's these kinds of little details that make testing a Laravel project so much fun!

If you want to up your testing game, check out our complete course about Testing Laravel. It teaches you how to test a Laravel application, from a beginner to master level.

(more…)

By , ago
PHP

How Livewire works (a deep dive)

The experience of using Livewire seems magical. It’s as if your front-end HTML can call your PHP code and everything just works. A lot goes into making this magic happen. Read more

By , ago
PHP

How to start and stop polling in React and Livewire

[AdSense-A]

Last week, support for Telegram notifications was added to both Flare and Oh Dear. Let's take a look at how both these services connect to Telegram. We'll focus on how polling is used in the connection flow and how polling can be stopped once the necessary data has been received.

Explaining our use case

Flare is an exception tracker made for Laravel applications. Under the hood, it is built using Laravel / Inertia / React.

Let's take a look at how people can connect to Telegram. At the Telegram notification preferences at Flare, users find the instructions. A bot needs to be invited to the Telegram room where notifications should be sent to. The bot is started by messaging a token.

Here's how that looks like.

animation

Don't bother trying to use that token. It is randomly generated, based on some secret user data, and has a very short lifespan.

Whenever a command is sent to the @FlareAppBot, Telegram will forward the command to Flare via a webhook. That webhook also contains a chatId. This id identifies the unique chat room / channel used in Telegram. Flare will use this id to send notifications to the correct room on Telegram.

In the animation above, you see that a confirmation message is shown in Telegram as soon a token is accepted. That confirmation message is sent in the request on our server that handles that webhook.

But, you've maybe also noticed that the UI on Flare itself also changed. A message "Your Telegram chat has been connected successfully" is displayed. Let's take a look at how our UI knows that a chatId was sent.

The request that handles the webhook from Telegram will put the incoming chatId in the cache. The cache key name will be generated based on something user specific, so no chatId can be stolen by other users.

Let's now focus on getting that chatId from the cache to the UI. If you're only interested in Livewire, you can skip the following paragraph on React.

Start and stop polling in React

The Flare UI is constantly polling an endpoint that takes a look if something is in the cache.

Here's how that controller looks like:

namespace AppHttpAppControllersNotificationDestinations;

use AppDomainNotificationTelegramActionsTelegramInviteToken;

class TelegramNewChatIdController
{
    public function __invoke()
    {
        $token = new TelegramInviteToken(current_user());

        $chatId = '';

        $cacheKey = $token->cacheKey();

        if (cache()->has($cacheKey)) {
            $chatId = cache()->get($cacheKey);
            
            cache()->forget($cacheKey);
        }

        return response()->json(['new_chat_id' => $chatId]);
    }
}

If we open up the browser's dev tools, we see that we're constantly polling for that chatId.

animation

When a new chatId is received, the UI is updated with that confirmation message. Also, notice that after a chatId is received, we stop polling. Let's take a look at the part of the React component that is responsible for that behaviour.

if (telegramChatId) {
    return (
        <>
            <Alert>
                Your Telegram chat has been connected successfully!{' '}
                {!hasInitialTelegramChatId.current &&
                    'Please submit this form to save your settings and start receiving notifications.'}
            </Alert>

            <input type="hidden" name="configuration[chat_id]" value={telegramChatId} />
        </>
    );
}

return (
    <div>
        <div className="flex flex-wrap">
            <span>You can get started by inviting</span>{' '}
            <CopyableCode className="mx-1">@FlareAppBot</CopyableCode> to your Telegram chat, and issuing this
            command:
        </div>
        <CopyableCode className="mt-2">/start@FlareAppBot {telegramInviteToken}</CopyableCode>
        <Alert className="mt-4">
            <i className="fas fa-spinner animate-spin mr-2" /> Waiting for connection
        </Alert>
    </div>
);

You don't need to understand the entire code. The important bit is that telegramChatId is used to decide which message is displayed in the UI. The telegramChatId is also put in a hidden form field that will get sent to the server when that form gets saved.

To poll for the chatId, we have this code a bit higher up in the same component.

useInterval(
    async () => {
        const response = await axios.post(telegramEndpoint);

        if (response.data.new_chat_id) {
            setTelegramChatId(response.data.new_chat_id);
        }
    },
    2000, // delay in milliseconds
    !telegramChatId,  // set to false to stop polling
);

Here you can see that we use a hand-rolled useInterval function that takes a function that uses axios to check if the chatId was received. setTelegramChatId is part of a React state hook that will set the telegramChatId.

Let's look at the code of that useInterval function.

import { useEffect, useRef } from 'react';

export default function useInterval(callback: Function, delay: number, enabled: boolean = true) {
    const savedCallback = useRef<Function>(() => {});

    useEffect(() => {
        savedCallback.current = callback;
    }, [callback]);

    useEffect(() => {
        function tick() {
            savedCallback.current?.();
        }

        let id: any;

        if (enabled && delay !== null) {
            id = setInterval(tick, delay);
        }

        return () => clearInterval(id);
    }, [delay, enabled]);
}

We're using a React effect here, which is used to handle side effects (in our case making API calls). Our callable that performs the axios request is wrapped in savedCallback.current and made callable via the tick() function. JavaScript's native setInterval is used to execute that tick every a few seconds (specified by delay).

The closure inside useEffect returns () => clearInterval(id). This is called the clean up function. It will be executed whenever one of the dependancies given to useEffect changes.

Whenever that axios call receives a response with a chatId, telegramChatId in the component that calls setInterval will be set to something, causing enabled to be set to false in setInterval. The dependencies of useEffect change, causing that clearInterval to be called. Pretty neat!

There's a bit more going in the useInterval, but let's ignore that for now. The extra cruft is needed to make useInterval work in hooks. If you want to know more details, read this extensive blog post on using setInterval in React hooks by Dan Abramov.

Start and stop polling in Livewire

The UI of Oh Dear is built using Livewire. Previously, I shared how we use Livewire to build complex forms in Oh Dear.

Here's how the Telegram notifications look like at Oh Dear. Just like on Flare, a user must copy/paste a specific token in a Telegram chat to get started. Notice that we are polling for an incoming chatId. When the component detects a chatId in the cache, a confirmation message is displayed, and the polling stops.

animation

Here's the Blade view of the TelegramChatIdComponent Livewire component that does the polling and renders the message.

<div class="mb-4">
    @if(! $chatId)
        <div
            wire:poll.keep-alive="pollForChatId()"
            class="p-4 bg-orange-200 border border-orange-300 rounded"
        >
            Perform these steps in the Telegram group where you want to receive notifications:

            <ul class="mt-2">
                <li>1. Add the Telegram bot "OhDearApp"</li>
                <li>
                    <div class="max-w-8">
                        2. Copy and paste this command

                        <x-jet-input type="text" readonly :value="' /start@OhDearAppBot ' . $token"
                                     class="mt-4 bg-orange-100 px-4 py-2 rounded font-mono text-sm border-orange-200 text-orange-900 w-full"
                                     autofocus autocomplete="off" autocorrect="off" autocapitalize="off"
                                     spellcheck="false"/>
                    </div>
                </li>
            </ul>
        </div>
    @else
        @if($isNewChatId)
            <div class="p-8 bg-green-200 border border-green-300 rounded text-green-900">
                You've connected Oh Dear to Telegram. You should now save this form.
            </div>
        @else
            <div class="p-8 bg-blue-200 border border-blue-300 rounded text-blue-900">
                Telegram has been connected.
            </div>
        @endif
        <input
            type="hidden"
            name="notification_destinations[{{ $index }}][destination][chat_id]"
            value="{{ $chatId }}">
    @endif
</div>

Livewire has built-in support for polling. Polling can be started by adding the wire:poll directive to an HTML element.

wire:poll.keep-alive="pollForChatId()

Our component will execute the pollForChatId() function every two seconds (that's Livewire's default polling frequency).

By default, Livewire will stop polling whenever the browser or tab where a component is displayed is not active. In our case, the user will probably have switched to Telegram, but we still want to have the confirmation message rendered as soon as possible. That's why we added the keep-alive option, which makes sure that we keep polling, even if the browser is not active.

Let's take a look at that pollForChatId() function on the TelegramChatIdComponent.

 public function pollForChatId()
{
    $cacheKey = $this->getInviteToken()->cacheKey();

    if ($chatId = cache()->get($cacheKey)) {
        $this->isNewChatId = true;

        $this->chatId = $chatId;

        cache()->forget($cacheKey);
    }
}

You can see that when we find a chatId in the $cacheKey of the cache, we set that value to $this->chatId. Because we change something to the public properties of the component, the Livewire component will re-render.

In the Blade view, you might have noticed that we only render that div containing the wire:poll directive whenever chatId isn't present.

@if(! $chatId)
    <div
        wire:poll.keep-alive="pollForChatId()"
        class="p-4 bg-orange-200 border border-orange-300 rounded"
    >
	{{-- other stuff omitted for brevity --}}
@endif

So with $chatId now set to a value, that div won't be rendered anymore. And because the div isn't rendered anymore, Livewire will stop polling. That's a pretty easy way to stop polling. To me, this is one of the many great details that Livewire creator Caleb Porzio added to Livewire. You don't have to think about it. Livewire does the right thing.

In closing

I hope you've enjoyed reading both approaches on how to start and stop polling. If you want to see this in action for yourself, create a free account on either Flare or Oh Dear. Both services offer a free 10-day trial.

Flare is the best exception tracker service for Laravel apps. Whenever an exception occurs in your production environment, we can send you a notification so that the exception doesn't go unnoticed.

Oh Dear is an all-in-one monitoring solution for your apps. It can notify you when your site is down, when your certificate is not ok, and whenever any of your app's pages contain broken links / mixed content. It can also monitor your scheduled jobs, and public status page, (like this one from Laravel), can be created.

Both services can notify you via Telegram and via mail, SMS, Slack, Webhooks, and a couple more services.

Did you notice that the React, PHP, JS and Blade code snippets in this post were perfectly highlighted? This is done using our free spatie/laravel-markdown package, which uses Shiki under the hood. Be sure to also take a look at our many other open source packages. Pretty sure there's something there for your next project.

(more…)

By , ago
PHP

A Project Manager’s Top Tips

Just as each workday is a little different, the same can be said about digital projects. Some digital projects are big and require large teams, months of collaboration, and brand new everything to bring them Read more…

By , ago