Laravel 12.5, weekly updates, and weekly tip

Laravel 12.5

Two major releases and a few minor releases bring us up to Laravel 12.5. Here are the highlights.

  • Add AsHtmlString cast in #55071
  • Add Arr::sole() in #55070
  • Add Model::except() to exclude attributes in #55072
  • Add #[Scope] for local model scopes in #54450
  • Add assertDoesntThrow() in #55100
  • Add whereNull() and whereNotNull() to AssertableJson in #55131
  • Add optional shouldRun() to migrations in #55011
  • Add pipe() to query builder in #55171

You may review the full branch diff on GitHub for a complete list of changes.

This version bump and update is automated for subscribers to a Shifty Plan. If you don't have one of those, be sure to bump your constraint and run composer update to get the latest features.

Weekly Journal

Last week I mostly proofread BaseCode. My high school computer teacher reached out and wants to giveaway some signed, printed copies to her graduating students. I'm honored. Plus, I've always wanted a copy for my bookshelf. So I'm jumping on the opportunity.

There's a minimum order of 50. So I'll have about 25 copies leftover. I'm thinking about bringing them to Laracon US to sign and sell (~$20). Let me know if you'd be interested in a copy. Maybe I'll print more...

To start this week, I refactored a service class within WP Static. JT and I were both starting to feel the pain of this class. Now that we are adding more features, some of the early abstraction was coming back to bite us. So I took a few hours to clean it up.

In the process, I put my testing hat on and got deep. This resulted in a PR to the docs, a clarification, and today's tip.

Weekly Tip

The main refactor of the service class was to limit the values passed into the constructor and instead pass them to the methods as needed. In our test, we were mocking the service with Laravel test helpers (which call Mockery underneath).

$service = $this->mock(CloudflareService::class);
$service->expects('createPagesProject')
->with($name, $repository)
->andReturn($url);

Despite this mock setup, the expectation was failing. After some head scratching and a few dump outputs, we realized the implementation was not using the mock.

We were resolving it from the container.

resolve(CloudflareService::class, ['token' => $site->api_token]);

Now if you've run into this in your own testing, you probably know the issue. If not, let's go deeper down the rabbit hole together.

On the surface, we are resolving it from the container and mocking it in our test. So the wiring should be correct. But it's indeed resolving a real CloudflareService.

Under the surface, this has to do with how the container actually works. Specifically when resolved with and without parameters.

When resolved without parameters, Laravel can cache the instantiation of the object as it doesn't change between resolving. But when resolving with parameters, the object can change between resolving.

The gotcha is the mock helper (and others) doesn't override this internal behavior of the container. Said another way, they doesn't mock anything resolved with parameters.

To do that, you have to register your mock with the container.

$service = Mockery::mock(CloudflareService::class);
$service->expects('createPagesProject')
->with($name, $repository)
->andReturn($url);
 
$this->app->bind(CloudflareService::class, fn () => $service);

Using bind puts our mock into the container so it is resolved even when instantiated with parameters.

This is a bit of a nasty gotcha. It requires intimate knowledge of the container to properly write your test. I think most would have expected the mock helper to just work. At least I did.

I'm thinking about how I might adjust the mock helper to avoid this gotcha. Maybe an optional always parameter. Maybe another explicit helper method. Or maybe I'll just add something to jasonmccreary/laravel-test-assertions.

View Archives →