PHPUnit and Laravel Config Caching

This week at work, I ran into a fun issue with a colleague while running some unit tests that I wanted to document here.

I was writing some new unit tests for a legacy codebase that’s been around a long time. For the sake of portability, I decided to use an in-memory SQLite database. After writing the tests and finishing the other work for the ticket, I opened a PR and went about my other tasks.

My colleague pulled down my ticket for testing and ran the tests. What should have been a very simple test was failing! What was weirder, is that the failing action was happening during one of the database migrations. The error isn’t important but for the sake of completeness, but it was a foreign key constraint error on the database.

After trying to debug stuff with them, we also noticed that his local database had been entirely nuked. Considering the test was running with the RefreshDatabase trait, I was starting to get an idea of what was going wrong.

What happened?

The empty database was the big clue that led me down the rabbit hole. I knew that running the database migrations is a slightly different process for in-memory vs. a real database, so I dug into that trait first.

This method in the call stack stuck out to me:

protected function usingInMemoryDatabase()
{
	return config('database.connections')[
		config('database.default')
	]['database'] == ':memory:';
}

If the config value is for the default database is :memory: then this would return true. Since PHPUnit overrides your .env when the tests are ran, this should be the correct value. Dumping it however, we get a different result.

For the config() function to be returning a different result than what is in a configuration file already, that must mean a config cache file exists. You can read more about how Laravel potentially includes that file here.

In our case, there was a config cache present, so when Laravel was booting to run the tests, that config was being used instead of PHPUnit’s environment variables.

To tie it back to the error we were seeing, since the local MySQL database was being used instead of the in-memory database, a migration with some outdated seeding code was throwing the foreign key constraint error. This is also a good reminder that MySQL and SQLite are not the same! If you are expecting foreign key constraint failures to be enforced in your tests, use the appropriate database for both your test suite, and when running your application.

The Solution

If this case, I decided it was best to try and fix the problem for the user running the tests. I modified the base TestCase.php as follows.

<?php

namespace Tests;

use Illuminate\Support\Facades\Hash;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__ . '/../bootstrap/app.php';

        $app->make(Kernel::class)->bootstrap();

        Hash::driver('bcrypt')->setRounds(4);

        $this->clearCache($app); // Added this line.

        return $app;
    }

    protected function clearCache(Application $app)
    {
        // We don't have a cached config, so continue running the test suite.
        if (!$app->configurationIsCached()) {
            return;
        }

        $commands = ['clear-compiled', 'cache:clear', 'view:clear', 'config:clear', 'route:clear'];
        foreach ($commands as $command) {
            \Illuminate\Support\Facades\Artisan::call($command);
        }
        // Since the config is already loaded in memory at this point, 
        // we need to bail so refresh migrations are not ran on our
        // local database.
        throw new \Exception('Your configuration values were cached and have now been cleared. Please rerun the test suite.');
    }
}

If no config cache file is found, then we’re golden! Continue running the test suite. If we do find a config file, we run a few Artisan commands to clear all of those files and then throw an Exception. The exception is necessary since the config is already loaded into memory at this point. If the tests continued to run, all of the requested config values would be the old ones. While it may be possible to reset the config and continue running the tests, the utility of that is miniscule. Instead, we inform the user of the action that was taken and ask them to run the test suite again.

In an ideal world, you would never have a configuration cached locally as it’s main purpose is to speed up expensive disk operations in production, but alas, we don’t live in an ideal world! 😅


I want to give a shoutout to Joel Clermont for helping me debug what was going on and being a great rubber 🦆!

How to forward catch-all email after canceling Google Apps subscription

I am doing some digital house cleaning and I’ve wanted to cancel a Google apps business account for a while now but the ease of using the catch all domain names has been great and I didn’t want to give that up. Plus the interface for Gmail isn’t that bad.

I know there are paid services out there that offer similar functionality and I was ready to buy one until I came across this!

Enter https://forwardemail.net/ 🎉

It’s an open source and free email forwarding service that can handle catch all addresses. Setup is simple. Add some MX records, then add a TXT record with a forwarding address. This does make your forwarding address public but since I use Hey I have pretty good control on who is allowed to email me thanks to The Screener.

Just in case, I did change the few accounts still using those custom domains, but I’ve tested it and so far within about 20 minutes things seem to be propagating nicely. I’ll be keeping an eye on it over the next few weeks but an hour in and I’m pretty happy!

Check it out!

Health update – March 2021

Been a hot minute since my last update! To those of you that reached out to check on my progress, I am very appreciative and touched that you take the time out of your day to think of me. It’s encouraging to my soul. ♥️

Since my last update in January, things have been pretty a-ok. I’ve been seeing a therapist since that time and while I don’t feel that I’ve come to any sort of dazzling revelation, I keep scheduling appointments at the end of every session. I think part of what I’m latching onto is the accountability of talking to someone. I pay this person $100+ a month to listen to me give progress reports of my mood and feelings, and my physical health obviously plays a big role in that. Maybe I would do just as well with one of those gambling apps where you put down some money to accomplish some task and if you fail, they don’t give it back?

Exercise

My Apple Fitness workout history

As you can see from the picture here, I’ve gotten into working out a little since the end last week of January. 95% of the time, this is riding the stationary bike in my building’s gym, but sometimes I’ll go for a long walk up and down the parking garages’ surrounding my house (it’s been cold though!). It’s light exercise by any standard; very low impact, and I usually stop soon after my exercise ring closes; about 30 minutes. In that time, the bike says I travelled 6-8 miles, depending on my pace.

The biggest break was the week of February 15, where Texas had the snow-pocalypse that cut power to millions of people and dozens died of exposure, indoors. While I won’t go into the politics of it all, needless to say that week was weird. My partner and I went to my parents house on Monday night and were there most of the week. My parents don’t have a gym and I didn’t want to go trek in the snow just for a workout so I stayed inside.

I can’t say that the workouts are providing a lot of physical benefit. My weight hasn’t dropped drastically (as will be evident soon) but I cannot definitely say there is no benefit at all. This past week I’ve been working out later in the evening, around 10-11pm, and largely have been motivated to that thanks to the support from some colleagues at work. A few of us have Apple Watches and getting the notification that someone closed all of their rings for the day is a surprisingly effective motivator. The fact that I am responding to that pressure in a positive way is probably a good thing. I can easily think of times past when I would have laughed at those notifications and went back to watching TV and eating an entire bag of Doritos (…now I want Doritos…).

As the weather begins to warm up here in Texas, I want to try taking my rides outdoors as I have acquired an actual bicycle from a friend. I’m interested to see how that goes over the next few months.

Diet

My diet has been generally the same as before and I’ve done a decent job of sticking to my targeted macros. In the past two weeks, I’ve fallen back into my Wingstop habit, which is not sustainable calorically or financially, but the dopamine hit has been undeniable at times.

My go-to dinner has become a large salad with 4-5oz of greens, an ounce or two of goat cheese, fresh strawberries, and a handful of nuts with some sort of low carb dressing. I pair that with some protein, usually chicken of some variety, about 6-10 oz if I had to guess. Chicken thighs are my favorite though.

When I do get Wingstop, I’ve now transitioned to only ordering chicken (without my usual large side of fries). That alone is a reduction of close to 1000 calories and 100 carbs when y0u factor in the ranch dipping sauce. If I have the ingredients available at home, I will first make a salad like I described above, eat that, then eat the wings. The greens soak up a lot of the grease and upsets my stomach less, while also helping me feel fuller. I noticed this last night as I finished eating around 8:30pm and didn’t feel compelled to eat more the rest of the night.

At 10pm, I noticed the time and thought I wanted to get a snack, but managed to talk myself into drinking a big glass of water instead. My usual after dinner snack can be 500-800 calories of roasted nuts so great calorie savings there.

I still have a weakness for snacks, which I tend to eat after dinner but I think I’m doing better. For instance, I’ve started eating fresh strawberries as a snack which is surprisingly less bad than I originally thought. A pound contains 150 calories and 22g of carbs with about 8g of fiber. Those aren’t keto macros, but I can often feel stuffed after finishing a container. Satiety is my biggest goal when eating so that’s fine by me.

I’m still taken with how my tastebuds have changed in reaction to my decreased sugar intake. The strawberries now taste so sweet to me, it makes one part of my brain think that there’s no way they’re healthy, but they are! A few weeks ago I got a craving for a shake from a fast-food joint and it was insanely sweet. I still drank it, but I honestly haven’t had the craving since and writing about it now, I am unfazed.

If I had to rate my diet since the last entry to this series, I’d probably give it a solid 7/10. Not perfect, but not a total failure either. C+

Weight Loss

The graph tells most of the story, but I’ll elaborate. I’m not sure if I put numbers out there, but then end of 2020 was rough. As you can see, I gained 20 pounds in about 2-3 months, despite intermittent fasting 16+ hours a day during that entire time.

Since January, I’ve been back to my 5 meals a week eating schedule and making progress in a positive direction. At the end of January, I purchased a new bathroom scale which has been encouraging and like I mentioned earlier, I’m now also working out every day. My body fat percentage has decreased by about 4% in that time, which feels good to me. I’m currently just under 38% body fat, which is still a lot, but it’s miles better than the high 40s I was rocking in 2018.

February was basically flat in terms of weight loss for the entire month, with some ups around when the storm hit. Considering one night that week I ate an entire large pizza by myself, probably not the worst outcome! March has been better, though we’re only 6 days in.

Tonight is my dad’s birthday and we’re going to an all-you-can-eat steakhouse and I’ve already resigned myself to indulging on some mashed potatoes and likely a full pound of meat, but honestly, it’s probably not that big of a deal. This month, I’d like to try and focus on mindfulness when it comes to snacking as that’s where I believe the bulk of my hindering calories come from. The roasted nuts especially.

If I can get under 250 by the end of the month without immediately fluctuating above that number in April, I’ll be really happy I think. That’s 1 pound a week which should be totally doable and sustainable. If I can keep that up for a year, I’ll be entering 1-derland in time for Summer 2022. That’s exciting!

I think I mentioned this before but I’ll say it again because I’m thinking about it. Winter 2020 was huge bummer for me because I had done so well in 2019. I bought new clothes that I was excited to wear, especially the following winter, that I ended up not being able to wear comfortably at all without risking damaging them. That fucking sucked. If I have one goal for 2021, it’s to be able to wear my new coat comfortably all through next winter. No bulging buttons or getting too hot that I need to take it off after less than 5 minutes indoors.

Final Thoughts

Thank you to everyone who has supported me over the past few months. Thank you to my wife who doesn’t complain when I suggest Wingstop every time a discussion about dinner comes up. Thank you to my parents have been supportive of my lifestyle changes and don’t pressure me to eat when I don’t want to. I know there are a lot of people out there who have differing views on fasting for weight loss, and my thoughts are with them as we all navigate this rocky journey to better health together.

If you want to chat about how your life is going, food related or not, hit me up on Twitter. My DMs are open. Talkin’ to new people online is really fun for me and I’ve been told I’m an excellent sounding board.

✌️


Photo by Dovile Ramoskaite on Unsplash

Installing Laravel Spark Manually with Composer – 2021

For whatever reason, you may need to install the new Laravel Spark into your project without following their installation instructions. Sometimes you don’t control the server environment as much or devops, etc so it’s just easier to include the files in your project. Here’s how to tell Composer where the files are and how to load Spark.

First – Update composer.json

Similarly to the official install docs, you need to add a snippet to your composer.json file.

"repositories": [
   {
     "type": "path",
     "url": "./spark-stripe"
   }
],

In my case, I have a folder in the root of my project called spark-stripe in which I placed all of the package files. Does it matter what the name is? I don’t know but since that is the package name, it made the most sense to me.

Final – Install the Package

Lastly, you’ll need to install the package like you normally would. For what it’s worth, I’m using Composer v2.

composer require laravel/spark-stripe 

If you don’t see a new list of dependency packages being installed, you likely did something wrong. Go back and check your composer.json file for spelling errors or other mistakes.

Hope this helps you out!

How to add a Macro to the Laravel HTTP client facade

While working on Let Them Eat 🍰I came across some peculiar behavior. For some of the Slack web API’s, there is a requirement to send the requests as a URL encoded form object. Since all of their API endpoints support this method of access, I created a little helper on my user object to get an instance of the Laravel HTTP client, with the asForm() method already applied.

This was working great until today when I wanted to add support for blocks to one of my bot responses. While I could support sending that field as a JSON string, it felt better to change the request to be sent fully as JSON instead. I thought, that would be as simple as calling ->withHeaders() again, but unfortunately, the deep-recursive-merge used by the HTTP facade doesn’t clear out any existing values.

Http::asForm()->asJson()

"headers" => [
  "Content-Type" => [
    0 => "application/x-www-form-urlencoded",
    1 => "application/json"
  ]
]

Obviously, the best option would be to not call the facade with multiple methods that change the same object, but in this case I really wanted to just overwrite it for this one instance.

Enter, Macros!

There are lots of places online to read about Laravel Macros so I won’t go into it here too deeply, but the gist of it, is you can add custom methods to core objects without extending them and creating new classes. This can be super helpful when you just want to add a little helper method but don’t want to go through the process of extending the class the old-fashion way. This can be especially useful to access protected properties or private methods.

I knew all facades in Laravel are macroable so I jumped into my AppServiceProvider boot method and added a lil somthin somethin.

// within AppServiceProvider::boot()
PendingRequest::macro('clearContentType', function () {
  $this->options['headers']['Content-Type'] = [];
  return $this;
});

Originally, I tried to add the Macro directly to the Http facade, but that only ended up working if I called the new method statically. To have it work in the manner I expected, I had to add the macro to Illuminate\Http\Client\PendingRequest, which is the class that is bound to the Http facade under the hood.

Using my new macro, I can easily clear out any content type headers before making a request, no matter how many times I call methods that set the content type header.

Http::asForm()->asJson()->asForm()->asJson()->asForm()->clearContentType()->asJson()

"headers" => [
  "Content-Type" => [
    0 => "application/json"
  ]
]

Now, I suspect this is a bug, but I’m not sure. I’ll be opening an issue on Github and we shall see. In the meantime, the macro will have to do! 🙂