PHP
Trees and Tree Traversal in PHP
In this post I want to introduce you to Tree structures. What they are, how you can use them, and in which situation they can be helpful. Read more
In this post I want to introduce you to Tree structures. What they are, how you can use them, and in which situation they can be helpful. Read more
[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.
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
It was as if someone had “pulled the cables” from their data centers all at once and disconnected them from the Internet. Read more
[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.
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.
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.
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
.
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.
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.
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.
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…)The Adapter pattern and the Bridge Pattern have brought along a lot of confusion. Time to connect the dots. Read more
At Flare, we launched a new search experience. We have a very cool search field with autocompletions (powered by the rendering engine behind VS Code), and a very smooth graph powered by AmCharts. Let’s take Read more…
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…
[AdSense-A]
This edition of the meetup features those two excellent talks:
You can watch recordings of previous editions in this playlist on YouTube.
(more…)Enhance (final) classes and functions by implementing these similar, but not quite the same, patterns. Read more