Skip to content

Category: Laravel

Setting a default sort order with Laravel Nova

If you’re using Laravel Nova to create a dashboard to manage your resources, you’ve almost certainly ended up looking at this code from https://github.com/laravel/nova-issues/issues/156#issuecomment-415974405

  public static $defaultSort = ['id' => 'asc'];
    
    /**
     * Build an "index" query for the given resource.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public static function indexQuery(NovaRequest $request, $query)
    {
        if (static::$defaultSort && empty($request->get('orderBy'))) {
            $query->getQuery()->orders = [];
            return $query->orderBy(static::$defaultSort);
        }
        return $query;
    }

This is the accepted way to set a default sort order for your resource. It’s great as far as it goes, but what if you want to sort on multiple columns? The code above assumes $defaultSort contains a single array. That’s not gonna work.

    public static function indexQuery(NovaRequest $request, $query)
    {
        if (static::$defaultSort && empty($request->get('orderBy'))) {
            $query->getQuery()->orders = [];
            foreach (static::$defaultSort as $field => $order) {
                $query->orderBy($field, $order);
            }
        }

        return $query;
    }

This function loops over the entire array and adds each entry to the query, so we can define our default sort as

    public static $defaultSort = [
        'group' => 'asc',
        'id' => 'asc'
    ];
1 Comment

Static Routes in Laravel

While we all hope to spend our time developing interesting sites and apps, sometimes a site just needs some static pages. Maybe it’s the terms of use or the privacy policy, maybe the site you’re working on is mostly a brochure site. Either way, having a /routes/web.php file full of

    // about page (resources/views/about.blade.php)
    Route::get('about', function()
    {
        return View::make('about');
    });

is tedious as heck.

Instead of repeating that block of code endlessly, there’s a handy snippet you can add to your /routes/web.php file to display all your static pages without having to think at all.

Route::get('{level1}/{level2?}/{level3?}', function (...$args) {
    $path = implode('.', $args);
    if (view()->exists($path)) {
        return view($path);
    }
    abort(\Illuminate\Http\Response::HTTP_NOT_FOUND, "Page $path not found");
});

(If you haven’t seen (...$args) before, it’s known as the splat operator.)

(If you’re using response codes like 404 directly in your code, consider using constants on the Response class instead – aside from the good habit of not including magic numbers in your code, it keeps you from spending all day looking up HTTP status codes.)

This should be the last route in your /routes/web.php file and it serves as a catch-all for any routes which haven’t been matched yet. It looks for a view file which matches the url and displays it. If no matching view is found, it returns a 404.

Using this snippet requires that your view files and folders mirror the structure of your site – http://mysite.com/small-print/privacy is going to look for a view in /resources/views/small-print/privacy.blade.php.

Leave a Comment

Injecting mocks in Laravel tests

If you’re doing much testing in Laravel, you may have found yourself writing a lot of code like this for mock objects:

$mockService = Mockery::mock(PaymentService::class);
$mockService->shouldReceive('voidPayment')->andThrow(new PaymentException("Couldn't void payment"));
app()->instance(PaymentService::class, $mockService);

Sometimes you’ll find yourself duplicating versions of this code throughout a class if you’re testing a lot of interactions with an outside service. It’s not a big deal, but it’s a little tedious. (Also, I tend to forget the last line and not understand why we’re hitting the real service instead of the mock.)

I’ve made myself a little shortcut:

// TestCase.php
protected function bindMock(string $className, callable $configuration)
{
    $mock = Mockery::mock($className);
    call_user_func($configuration, $mock);
    app()->instance($className, $mock);
}

Once that code is in my base TestCase class, I can use the following code in my test cases:

$this->bindMock(PaymentService::class, function ($mock) {
    $mock->shouldReceive('voidPayment')->andThrow(new PaymentException("Couldn't void payment"));
});

It’s not a huge simplification, but I do think it’s a little easier to read and easier to not screw up.

Leave a Comment

Extending Laravel’s Functional Tests with Macros

I was fiddling around with a new project and I wanted to add a quick functional test to make sure that entries were appearing in the order that I expected. I know that neither Laravel nor PHPUnit have an assertion for that built in, but I figured it would be no problem to add it to the base TestCase class.

Bzzzt, sorry, nope, thanks for playing.

In the pre-Laravel 5.4 days, I would have created a function called seeThisBeforeThat() on the TestCase class and been all set.

$this->visit(‘/entries/’)->seeThisBeforeThat(‘Entry 1’. ‘Entry 4’);

With 5.4, though, we mostly don’t test assertions against the TestCase class, we test them against the Response that’s returned when we get() or post().

OK, no sweat, I’ll just extend the TestResponse class to add an extra assertion. (Did you know that when you’re testing, the Response that you get back isn’t a standard Illuminate\Http\Response? The Response is wrapped in a class called TestResponse which is where the assertions live.) Extending the TestResponse class will work as long as the Response is instantiated through the IOC container and not … oh, it’s instantiated directly.

Stuck, right? Wrong.

A recent blog post (https://unnikked.ga/understanding-the-laravel-macroable-trait-dab051f09172#.lcxjul9oz) reminded me that lots of Laravel classes implement the Macroable trait and TestResponse is one of them. Macros, for those who haven’t used them, allow you to add functions to existing classes without making a new class. (The Laravel docs are a little spotty on macros – https://laravel.com/docs/5.4/responses#response-macros is all there is and it doesn’t explain how widely available macros are. Extending those docs could be a good first PR for someone.)

It’s precisely what I need in a case where I can’t inject an extended TestResponse class but want to add a quick function to it.

Here’s what I did:

In AppServiceProvider, I added the following code:

public function register()
    {
        if ($this->app->environment('testing')) {
            $this->addTestResponseMacros();
        }
    }

    private function addTestResponseMacros()
    {
        TestResponse::macro('assertThisBeforeThat', function($earlier, $later) {
            // since we want this to fail if either isn't there, assert that we see stuff.
            $this->assertSee($earlier);
            $this->assertSee($later);
            $body = $this->getContent();
            return PHPUnit::assertGreaterThan(
                strpos($body, $earlier),  
                strpos($body, $later),
                "{$earlier} does not appear before {$later}"
            );
        });
    }

And also … oh, wait, no, that’s it.

The test is pretty straightforward:

/** @test */
    public function guest_can_view_entry_list ()
    {
        // ... setup code to make a list of entries

        $response = $this->get('/entries/');

        $response->assertStatus(200)
            ->assertThisBeforeThat('Entry 1', 'Entry 4');
    }

Is there something else you want to assert() about the response? Just make a macro.

1 Comment

In Which … J.T. learns how traits work

On the home page of the site I’m building right now, the user is asked to enter a city and state.

Homepage City/State Entry

I’m using Laravel 5.1’s super-simple validation to make sure that both fields are entered:

       $this->validate($request, [
            'facilityCity'=>'required',
            'facilityState'=>'required'
        ]);

That’s all it takes to (1) make sure both fields are present and (2) display an error if they’re not.  The only problem for me is that when validation fails, the user is redirected to the previous page. 99% of the time, this is the desired behavior, but the one place I don’t want it is on the home page. Displaying errors there just ruins the layout.

Here are the highlights from Laravel’s ValidatesRequest trait:

public function validate(...)
{
    ...
    if ($validator->fails()) {
        $this->throwValidationException($request, $validator);
    }
}

protected function buildFailedValidationResponse(...)
{
    ...
    return redirect()->to($this->getRedirectUrl())-> ...
}

protected function getRedirectUrl()
{
    return app('Illuminate\Routing\UrlGenerator')->previous();
}

So if we can just change getRedirectUrl(), everything else should fall into place. Obviously, we don’t want to change anything in the core Laravel files. Can we override a trait function?

190px-Obama_logomarkYES WE CAN

<?php namespace App\Validation;

use Illuminate\Foundation\Validation\ValidatesRequests as BaseValidatesRequests;

trait ValidatesRequests
{
    protected $redirectOnError='';

    use BaseValidatesRequests;

    /**
    * Get the URL we should redirect to.
    *
    * @return string
    */
    protected function getRedirectUrl()
    {
        $urlGenerator = app('Illuminate\Routing\UrlGenerator');
        if ($this->redirectOnError) {
            return $urlGenerator->to($this->redirectOnError);
        }
        return $urlGenerator->previous();
    }
}

Traits don’t use inheritance (we can’t use the extends keyword), but they compose very nicely. By creating a new trait which uses the base ValidationRequests trait, we have all the functions available in that trait and can override one.

Also note the new $redirectOnError variable – we only redirect if this is set.

The base Controller class is updated to point to this new ValidatesRequests trait and all the inheriting controllers now have this feature available.  The new function just has one extra line:

$this->redirectOnError = 'signup/city_state';
$this->validate($request, [
    'facilityCity'=>'required',
    'facilityState'=>'required'
]);
Leave a Comment

require-dev in Laravel

THIS POST IS NOW OBSOLETE. This post referred to Laravel 4, and the information was always a little iffy. This post remains only to prevent this blog from being entirely empty.


 

Note: this post, like all Laravel tips, is best read in Jeffrey Way’s voice.

There are some packages (Jeffrey Way’s Generators and Barry van den Heuvel’s IDE Helper come to mind) which we only need during development, but which become tightly coupled with Laravel. Once you’ve added the packages to the array of ServiceProviders in app/config/app.php, you have to either have a different config for development or include those packages in your deployment. Deploying them isn’t going to do any harm, but I think it’s a good habit to keep your deployment as lean as possible — the fewer packages on your production server, the fewer things that can go wrong.

One way to keep these packages from having to be deployed is to have a different config file for your local machine. Laravel certainly supports this, but I’m not wild about it as a solution to this particular problem. There’s (presently) no way to merge an array of service providers in the main config/app.php file with one in config/local/app.php. You’d have to maintain two lists and make sure that you keep them in sync. Add a service provider in one file, and it’s up to you to remember to add it in the other.

EDIT:Matt Stauffer points out that Laravel already has a built-in helper function for this: append_config. Check out the Laravel docs for more detail.

[And now we return to the original, obsolete post]

I’ve come up with a different solution that I’m using — I don’t promise that it will work for you, but I don’t see why it wouldn’t.

Instead of adding service providers to the list in app/config/app.php, I manually register them from /app/start/local.php. That way, they’re only available during local development. Here’s an example with Jeffrey Way’s Laravel 4 Generators (which you should totally install.)

First, we add the package to our composer.json file as a development dependency:

"require-dev": { 
   "way/generators": "2.*" 
},

We run composer update --dev so that composer knows we want the development packages too.

The instructions for installing Generators tells us to add 'Way\Generators\GeneratorsServiceProvider' to the service providers array. That’s just what we won’t be doing. Instead, we’re going to edit /app/start/local.php and … add one line. Really, that’s all it takes:

<?php     $app->register( new \Way\Generators\GeneratorsServiceProvider($app));

Assuming that your environment correctly determines that it’s local (/bootstrap/start.php is your friend and so is Steve Grunwell’s article Laravel Application Environments without Hostnames), you’ll have php artisan generate commands available on your development machine without having to muck around when it’s time to deploy.

5 Comments