What I write about

I'm starting a new small project and I wanted to test out Tailwind Alpha while I'm writing boilerplate. I really wanted to use the new CSS theming since I use CSS variables anyway.

My major blocker would be using PostCSS Mixins. I use PostCSS Mixins to handle theming. It's not just light/dark mode, but theming in general.

To my surprise, even while early alpha, it works! Here's my test .pcss file.

@import "tailwindcss";
 
@define-mixin theme-light {
--color-canvas: #ff0000;
}
 
@define-mixin theme-dark {
--color-canvas: #00ff00;
}
 
@theme {
@mixin theme-light;
}
 
@layer base {
@media (prefers-color-scheme: light) {
.theme--device {
@mixin theme-light;
}
}
 
@media (prefers-color-scheme: dark) {
.theme--device {
@mixin theme-dark;
}
}
 
.theme--light {
@mixin theme-light;
}
 
.theme--dark {
@mixin theme-dark;
}
}

On the day, I upgraded Medley to Laravel 11 using Laravel Shift. almost perfectly smooth, it missed custom routing and my events, but nothing that I couldn't fix myself.

By the way, you can check if you can upgrade your Laravel application here! Can I Upgrade Laravel. Useful before you check out a Laravel Shift!

Alright, let's talk about some of the changes that caught my eye in Laravel 11.

Slimmed down configuration

L11 now ships with smaller config files. Basically, you can remove configs you don't need from your config and Laravel will do a recursive merge to provide those defaults.

Here's what my config/filesystems.php look like now.

<?php
 
declare(strict_types=1);
 
return [
 
'disks' => [
'r2' => [
'driver' => 's3',
'key' => env('CLOUDFLARE_R2_ACCESS_KEY_ID'),
'secret' => env('CLOUDFLARE_R2_SECRET_ACCESS_KEY'),
'region' => env('CLOUDFLARE_R2_DEFAULT_REGION'),
'bucket' => env('CLOUDFLARE_R2_BUCKET'),
'url' => env('CLOUDFLARE_R2_URL'),
'cdn_url' => env('CLOUDFLARE_R2_CDN_URL'),
'endpoint' => env('CLOUDFLARE_R2_ENDPOINT'),
'use_path_style_endpoint' => env('CLOUDFLARE_R2_USE_PATH_STYLE_ENDPOINT', false),
'visibility' => 'public-read',
],
],
 
];

I was able to remove anything Laravel provided by default and only add the custom ones I need.

I like this change a lot

I think one of the most tedious things when manually upgrading Laravel projects prior to 11 was keeping config files up to date. Especially the ones I don't end up using.

I jump between different projects a lot for my work. Configs for L11+ projects will now be "first stops" for myself to get a temperature check of the project.

I will miss just going around config files seeing what the other offerings are within the core though. I know I can always publish the config files via php artisan config:publish but when I'm working on my own projects, it's sometimes nice to just read around and discover something new.

Over all, I love this change! Makes maintenance and upgrades less of a slog.

Things I haven't tested or I'm assuming

  • I hope the package config files function the same. I would love to just only have package configs that I want to customize rather than having a lot of things I'll never touch. I'll test this out.

bootstrap/app.php

In L11, instead of having fragmented configuration between config/app.php and different Service Providers, a lot the configuration layer is moved to the bootstrap/app.php.

The bootstrap file is now where you configure:

  • Custom exception handling
  • Routing
  • Middleware
  • Events

I think this is over all a great change. It makes it another one of the first stops to get a feel of the application.

Because of this change, new L11 applications come with only a single AppServiceProvider.

My knee-jerk reaction to this was a bit negative since potentially bootstrap/app.php and AppServiceProvider could become huge files if I'm not too careful.

How it looks right now in Medley

For context, Medley is a medium sized application. It has about 246 endpoints and counting.

For this upgrade, I decided to lean in for now on L11 changes and let everything happen in AppServiceProvider and bootstrap/app.php.

Even with all that, AppServiceProvider is only 187 lines long and bootstrap/app.php is only 239 lines long. It's not a lot for a medium sized application!

I'm personally gonna move middleware registration and route registration to their own service provider though since that's the majority of the bloat of my app.php.

That's what I like with these updates in Laravel. I've been using Laravel since 3, professionally since 4.2 and a lot of the practices since 5.0, I can still do myself. The changes are just syntactic sugar.

Improved encryption key rotation!

This was the one I'm most excited about! I've personally been digging into Rails' DX on how they're handling key rotation to build a solution for Medley.

Then Laravel 11 ships with it!

For context, all of Medley's data is encrypted, so I've been struggling to figure out how to perform regular key rotations without having to do it when customers are asleep or taking the site down.

With the latest update, Laravel allows fallback app keys to attempt to use previous keys and re-encrypt data with the new cipher text.

It's an amazing improvement!

A couple of things I'm a bit unfamiliar with

To be clear, these things can either be unfamiliarity, not reading the docs, misunderstanding things, or lack of documentation. Nothing that can't be improved by learning a bit more, or opening a PR to the docs.

Exception Handling

After the upgrade, this was the change I had the most friction with. It took around 15 - 20 minutes so it wasn't too much of a head-scratcher, but I did have to source dive to figure stuff out.

Previously, projects came with a Exceptions/Handler.php file out of the box where you can customize how your project reacts based on specific exceptions.

Here are the custom exceptions I handle in Medley:

  • 419 errors should just redirect back and flash an error message for a toast message.
  • 403 and 503 errors are custom. I want them to use Inertia views so I can have better looking error pages.

With the new API, I couldn't find in the docs how the Request object was provided. It took a few minutes of source digging to see it's a third parameter on the $exceptions->respond() function.

Here's how my exception handling looks in the new L11 API.

->withExceptions(function (Exceptions $exceptions) {
$exceptions->respond(function (Response $response, Throwable $e, Request $request) {
if ($response->getStatusCode() === 419) {
toast()->error([
'title' => __('errors.session-expired.title'),
'caption' => __('errors.session-expired.caption'),
]);
 
return redirect()->back();
}
 
if ($request->isMethod('GET') && $response->getStatusCode() === 403) {
inertia()->setRootView('layouts.outside');
 
return inertia('errors/403')->toResponse($request)->setStatusCode(403);
}
 
if ($request->isMethod('GET') && $response->getStatusCode() === 503) {
inertia()->setRootView('layouts.outside');
 
return inertia('errors/503')->toResponse($request)->setStatusCode(503);
}
 
return $response;
});
})

It's nothing too different from the previous API aside from the source diving.

Event Handling

Now this one I think I just need to play around with the "new" events API. I've never liked event discovery so I've kept that off ever since been introduced.

So with the new API, I had to manually register all my event bindings via Event::listen() in my AppServiceProvider.

private function registerEventListeners(): void
{
Event::listen(HeartbeatAnswered::class, NotifyRecordSubscribers::class);
Event::listen(PostPublished::class, NotifyRecordSubscribers::class);
Event::listen(NodeCreated::class, NotifyRecordSubscribers::class);
Event::listen(StatusUpdateCreated::class, NotifyRecordSubscribers::class);
Event::listen(CommentCreated::class, NotifyRecordSubscribers::class);
Event::listen(TaskStageChanged::class, NotifyRecordSubscribers::class);
Event::listen(UserCreated::class, DownloadUserAvatar::class);
// ...
}

I don't quite like this, but I think I'm just doing something wrong here. 🤔

bootstrap/providers.php vs ->withProviders()

The L11 docs say any user-defined providers should be registered in bootstrap/providers.php. Although, Larvel Shift did add my custom providers to bootstrap/app.php via ->withProviders().

My assumption here is I'll just use bootstrap/providers.php but they're functionally the same.

I don't have anything too custom to have any motivation to dig further though!

Conclusion

It's overall an easy update! Aside from the exception handling and a named parameter change in Carbon 3, there wasn't any extreme changes that was bothersome.

I love the improvement in encryption. Making upgrades easier for future versions.

Hope you all have a smooth upgrade process too!

I'm still extremely mixed when it comes to AI that's coming out in the consumer market. Some are interesting. Some are fun. Some I cannot relate with. Some I feel strongly against.

Despite my pessimism, I explore what's out there.

This time it's Code Rabbit. It's a pull request AI which summarizes an open PR, and adds comments on code that it thinks needs to be reviewed by the author of the PR.

It's been day 2 of me using it. I gotta say, I like it so far. I think this could be useful for a lot of people. It actually comments on relevant pieces of code, it provides its rationale on why they're commenting on it, and opens suggestions to the PR.

I haven't merged any of its suggestions, but it does make me give a second look if what I did was intentional or accidental.

There are times where it doesn't really remember the context for the file though. I wrote this test case and it wrote three of the same review on different test cases that I should write a happy path test (which there is).

Over all, I think it's pretty useful. It's $15/mo so it's personally a bit too high for me since I'm mostly working on some small side projects.

Hope more of these types of tools show up though!

Browse for Me

March 2, 2024

Arc recently just released a feature called "Browse for Me".

I love it. But I'm also concerned about it.

Before I get into things, I'm open to accepting that I'm just ignorant about this emerging technology.

Why I love "Browse for Me"

As a consumer, I love using it. I can look up recipes, ask dumb questions, and I'll get the answer just like that.

I even noticed that I ask more dumb questions. Anything that piques my curiosity, I just ask Arc Search. The time between question and answer has been shortened. That made searching fun again.

Also looking up recipes has been so much easier. I've been cooking a bit more and making sure I cook food that matches my health conditions. I love trimming out the preamble and SEO manipulation of all these sites.

On the other hand...

In a world where Arc Search, Perplexity exists, what would incentivize people from writing?

I know there will be a lot of people who write for the sake of it. Writing to share what they know to the world. But it feels ass backwards. The people willing to share their knowledge, their expertise, or just their stories, exploited by machines that scrape and summarize their words.

What's the point of writing if these words will be distilled down, mixed with words from other sites, and then spat out as bullet points for someone to consume?

Do we really need to be that hungry about consuming everything online that we need to read everything? Listen to everything at 2x speed? Summarized? Distilled?

Why should one write when it's going to be summarized by a robot? Personally, when it comes to technical write ups, case studies, I know it's about the process.

Outro

I don't know, maybe I just don't understand this paradigm shift just yet.

Maybe this isn't just for me.

I hope what's being scraped doesn't go away so there's still room for the articles, blogs, and content that I like to enjoy.

Maybe at some point websites won't optimize for SEO but rather optimize for robot summarization. Or worse.. .both. Just a bunch of keywords and bullet points for for an machine to parse.

In writing tests, I tend to abstract things that aren't necessary for the specific feature that's under test. That way, the test case itself can read as documentation.

One of the biggest filler for test cases are setups of the test.

Let's start with a feature

To walk through this article, let's say we're building a feature to send a message to a Slack channel.

it('sends a message to a channel', function () {
$workspace = Workspace::factory()->create();
 
$user = User::factory()
->hasAttached($workspace, ['owner' => true])
->create();
 
$channel = Channel::factory()
->for($workspace)
->hasAttached($user)
->create();
 
actingAs($user);
 
 
assertDatabaseCount('messages', 0);
 
sendMessage($workspace, $channel, [
'content' => 'Good morning! 👋',
])->assertRedirect(
route('channels.show', [$workspace, $channel])
);
 
assertDatabaseCount('messages', 1);
 
assertDatabaseHas('messages', [
'content' => 'Good morning! 👋',
]);
})
 
function sendMessage($workspace, $message, array $attributes)
{
return post(route('channels.store', [$workspace, $channel]), array_replace([
'content' => 'Some valid content.',
], $attributes));
}

While straightforward enough, it adds up quite a bit the more test cases we need.

it('sends a message to a client', ...);
 
it('validates the data', ...);
 
it("doesn't allow non-channel members to send a message", ...);

beforeEach/setUp

To reduce the cognitive load of these tests, the common attributes are move up to the setUp method. Or in Pest's case, the beforeEach block.

Once we've moved the setup to the beforeEach block, our test would look something like this.

beforeEach(function () {
$this->workspace = Workspace::factory()->create();
 
$this->user = User::factory()
->hasAttached($workspace, ['owner' => true])
->create();
 
$this->channel = Channel::factory()
->for($workspace)
->hasAttached($this->user)
->create();
 
actingAs($this->user);
});
 
 
it('sends a message to a channel', function () {
assertDatabaseCount('messages', 0);
 
sendMessage($this->workspace, $this->channel, [
'content' => 'Good morning! 👋',
])->assertRedirect(
route('channels.show', [$this->workspace, $this->channel])
);
 
assertDatabaseCount('messages', 1);
 
assertDatabaseHas('messages', [
'content' => 'Good morning! 👋',
]);
})

Using a seeder

Looking at the set up, this won't be the only time we're going to write test data for:

  • Some random workspace.
  • Some random user in the workspace.
  • Some random channel.

To work with this, some developers use a seeder and use that to set up their tests.

beforeEach(function () {
$this->seed(TestAccountSeeder::class);
 
actingAs(User::first());
});

While this is totally valid, it personally doesn't sit right with me.

The seeders are a bit way out of reach, and the models need to be re-queried to be able to be usable in the test set up anyway.

Granted, I haven't spent a lot of time using this approach so I might be completely wrong on how developers are setting up their tests with seeders.

My preference: Traits

In Laravel, you can create traits that run during the setUp process by adding a method with setup prefixed to the trait name. Similiar to how bootable traits work with models.

I use these traits to set up the world depending on the context of a specific test case.

Here's how our test case would look.

# Fixtures/ActingAsRandomAccountOwner.php
trait ActingAsRandomAccountOwner
{
protected $workspace;
 
protected $user;
 
public function setupActingAsRandomAccountOwner()
{
$this->workspace = Workspace::factory()->create();
 
$this->user = User::factory()
->hasAttached($workspace, ['owner' => true])
->create();
 
actingAs($this->user);
}
}
 
# Fixtures/OnPublicChannel.php
trait OnPublicChannel
{
protected $channel;
 
public function setupOnPublicChannel()
{
$this->channel = Channel::factory()
->hasAttached($this->user)
->create();
}
}
 
# Feature/SendMessageTest.php
 
uses(ActingAsRandomAccountOwner::class, OnPublicChannel::class);
 
it('sends a message to a channel', function () {
assertDatabaseCount('messages', 0);
 
sendMessage($this->workspace, $this->channel, [
'content' => 'Good morning! 👋',
])->assertRedirect(
route('channels.show', [$this->workspace, $this->channel])
);
 
assertDatabaseCount('messages', 1);
 
assertDatabaseHas('messages', [
'content' => 'Good morning! 👋',
]);
})
 
# if using PHPUnit
 
class SendMessageTest extends TestCase
{
use ActingAsRandomAccountOwner, OnPublicChannel;
 
/** @test **/
function it_sends_a_message_to_a_channel()
{
// ...
}
}

I prefer this over seeders since I immediately have access to these class properties instead of having to requery them in the setup method. This also allows me to compose different traits depending on the context of the test.

  • OnPrivateChannel
  • ActingAsInvitedClient
  • AcinngAsWorkspaceMember
  • ActingAsSuperUser
  • etc...

This is just another tool in the toolbox

I'm pretty sure someone that's used seeders in tests knows an approach to do the same thing, this pretty much works well with how my mental model wants tests to look like.

At least for myself, seeders tend to be a way to set up the world for manual testing, either through deployments, local development, etc.

Let's say you have this setup:

class HandleInertiaRequests {
public function share ($request)
{
return array_merge(parent::share($request), [
'user' => "I am a user",
]);
}
}
 
class UsersController {
public function show(User $user)
{
return inertia('users/show', [
'user' => UserResource::make($user),
]);
}
}

In your inertia view, what you'll receive is the values from the controller even in your page store.

<!-- users/show.svelte -->
<script>
import { page } from '@inertiajs/svelte'
 
export let user // UsersController
 
$page.props.user // UsersController
</script>

Why does this happen?

This happens because the way inertia works is it passes all of the shared data and the controller data in the same component.

Which it seems to prioritize the controller data over the shared data.

How to prevent this from happening

Personally, to avoid this from happening, I prefix any shared data with a $. Since javascript allows for it in variables in the first place, seems like a good compromise.

class HandleInertiaRequests {
public function share ($request)
{
return array_merge(parent::share($request), [
'$user' => "I am a user",
]);
}
}

That way in your page, you can avoid conflicts.

<!-- users/show.svelte -->
<script>
import { page } from '@inertiajs/svelte'
 
export let user // UsersController
 
$page.props.$user // I am a user
</script>

A Svelte caveat

Inside Svelte components, Svelte compiles variables prefixed with a dollar as store objects. So destructuring these properties might not work... I haven't actually tried!

I assume this would break.

<!-- users/show.svelte -->
<script>
import { page } from '@inertiajs/svelte'
 
const { $user } = $page.prop
</script>

Use derived stores to centralize shared data

Now what I do to both avoid this issue and centralize all shared data is to make a store that derives the shared data.

In my store/app.js file.

import { page } from "@inertiajs/svelte"
import { derived } from 'svelte/store'
 
export const user = derived(page, ($page) => {
return $page.props.$user
})

Then import the store from the page.

<!-- users/show.svelte -->
<script>
import { user } from '$store/app'
 
$user // I am a user
</script>

Whenever I sign up on services online, it always stands out when there are separate fields for the first and last name. With the projects I work on, I always use a single field for the name. When clients ask to split the name into multiple fields, it ends up in a pretty long discussion why I think it's unnecessary.

==This isn't a best practice, silver bullet, or anything like that. I know that there are different use cases for different requirements. This is mostly based on the scope of the projects I've worked on.==

Okay, let's set the scene

Here in the Philippines, almost every form you'll run into will have a first, middle, and last name. Here's a visual breakdown of my own legal name since the terms for the names will differ from place to place

Jose Andres Cruz Gauran

The middle name here is my mom's maiden last name. Some friends living in the US told me that there, without this distinction, my middle name would be Andres instead of Cruz.

Here's a fun fact: ==plenty of organizations here use the mom's maiden name as a security question for customer verification and some even just ask for the middle name!==

For more additional context, here are some stuff about the projects I've worked on so you can compare and contrast with what you're working on.

  1. We don't use the middle name or the mom's maiden name as a verification question.

  2. The most data customer I've worked in is around 5 million rows, so I can't really answer for use cases beyond that.

With that out of the way, let's get started.

The rationale for multiple name fields

These are the most common things I hear from clients whenever this comes up:

  1. What if we need analytics based on the first, middle, or last name?
  2. What if we need a specific search for the first, middle, or last name?
  3. What if we wanted to be more personal by using their first name?

Because of these "what-ifs" a lot of systems here are designed where the first, middle, or last names are required. Funnily enough, a lot of developers design it as such even though there's a chunk of users that don't even have middle names here.

Let's hit off these points and see what the common responses I usually get.

“What if we need to search by the last name?”

Jose, a part of my first name, is a also a family name here. It's often argued that it saves a lot of time by splitting the names into fields so results won't be as much, or even have results from the first name.

While I do understand the sentiment, I think this should be more on improving the search implementation instead of immediately relying on splitting the name into different segments. For these cases, we can do sorting with relevancy. Maybe a left-to-right or right-to-left toggle?

There may be some cases that they want to know if there are family members with an account but that might be more of a security issue which is a whole other conversation.

“But won’t that slow down the search?”

I think we're at a point in tech where pretty small servers can do pretty optimized calculations because of the efficiency of the tools we have. We can still do performant searches in a table with a couple of million of rows.

I'll be honest, I've never worked on a project where searching specifically for first and last names has been critical feature. In customer support, the customer provides their full name for verification. Asking for a wildcard search is more of an edge case rather than a common happy path.

If it's about budget of implementing a better search, then there are services like Algolia that provide really really amazing search results with large chunks of data.

“What if we need analytics based on the name?”

This gets said more often than it should. Even when the client pulled rank and the name was segmented, we never really did any analytics with the name.

“What if we wanted to be more personal by using their first name?”

I either get the first part of the name, or add a nickname field in the user's profile. I personally don't really use my legal name. I use Jaggy publicly so reading emails that say my legal first name is pretty weird.

I think the same goes for a lot of people here. A lot of the people I know have a formal name that they don’t really use, or only use a portion of their “full name”. If that’s the case, then addressing someone with their full first name is still not as personable as people think it really is.

Let's talk about accessibility

Even though a lot of the apps will spend its whole life here in the Philippines, it's still likely to have customers that have migrated to this country. Yes, most of the customers will most likely be Filipinos, ==but it's not a good enough excuse to design the system in a very specific way.==

Not every culture in the world uses a family name.

I have a friend from Indonesia who has a lot of problems with local forms here since he didn't really have a first or last name. It's... a name.

Budget, Development, and Maintenance

I get some comments that if the user-base for the foreign customers is pretty small, it’s pretty fine to not support that specific format ‘cause of the time to implement a better search or something like that might increase the cost of development.

While I do understand where that comes from, I really don't think that implementing either is time consuming or complex. In reality, if we're using raw SQL statments, we'll end up with something like this.

# Start
SELECT * FROM customers WHERE name LIKE "Jose%";
 
# End
SELECT * FROM customers WHERE name LIKE "%Jose";
 
# Anywhere
SELECT * FROM customers WHERE name LIKE "%Jose%";

Cache the results, and it won't be as expensive as people make it out to be. If we want to do some sort of sorting on the code-side we can do something like this.

Customer::query()
->search($request->input('name'))
->get()
->sort(function ($customer) use ($request) {
return strpos($customer->name, $request->name);
});

This is me eyeballing the code so this might not really work. :sweat_smile:

Which should sort the name based on the position of the keyword. In descending order? I'm not quite sure.

Let me know what you think

I really hope that this somewhat made you reconsider having to split the name. If you know any specific cases where it needs to be separate, do let me know in Twitter. I'd love to know more so I can know when to do the same thing.

Thanks for taking the time to read!

I've recently come across the Laravel Websockets package. It's a Pusher replacement when it comes to web sockets.

For a bit, I've hit a few snags integrating it into some of projects I've been working so I wanted to document the process of setting things up, and making it work with Laravel and Nuxt from development to deployment.

Here are the things I want to hit off when working with the web sockets:

  • I want to work with HTTP locally, but HTTPS in production.
  • I don't want to continually change anything in my config, or .env files.

Here's my setup

[
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
'host' => env('PUSHER_APP_HOST', 'localhost'),
'port' => env('PUSHER_APP_PORT', 6001),
'encrypted' => env('PUSHER_APP_SCHEME') === 'http',
'scheme' => env('PUSHER_APP_SCHEME'),
'curl_options' => [
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
],
],
]

Things I want to work on

  • I don't like the fact that we use the actual PUSHER_ environment variable. It feels like we should be using a WEBSOCKETS_ prefix instead.
  • I want to try and help out with the docs 'cause there are parts of it that feels like we're using it alongside Pusher, rather than replacing it.