DEV Community

Matt Moore
Matt Moore

Posted on

6 Laravel Tips

I've put together a small list of tips that I've found useful while working with Laravel.

1. onDelete('set null')

You probably know about, and have used onDelete('cascade'); on foreign keys in Laravel migrations, but did you know about 'set null'? This allows you to preserve models when their related model is deleted. An example of this would be if you have comments that have a user, if the user gets deleted, and you want to preserve their comments, you can use onDelete('set null'). This will set your user_id column to null automatically if the user gets deleted, and you can handle this in your data presentation layer to show ‘User Deleted’ as the author, or something similar.

$table->foreign('user_id')
          ->references('id')->on('users')
          ->onDelete('set null');
Enter fullscreen mode Exit fullscreen mode

2. dropForeign([]);

For the longest time, I thought you had to use the full key name when rolling back a foreign key constraint in a migration. I was also confused when rolling back multiple foreign keys using array syntax didn't work.

// doesn't work
$table->dropForeign('user_id');

// works
$table->dropForeign('posts_user_id_foreign');

// doesn't work
$table->dropForeign([
    'posts_user_id_foreign',
    'posts_type_id_foreign'
]);
Enter fullscreen mode Exit fullscreen mode

When I read the documentation to figure out what was going on, I realized that the array syntax on dropForeign is for dropping a foreign key using a shorthand column name.

$table->dropForeign(['user_id'])
Enter fullscreen mode Exit fullscreen mode

Super handy! But why array syntax? What happens if you put multiple strings in there? I looked at the source code to find out since it wasn't obvious to me from the documentation.

Here is drop foreign:

/**
* Indicate that the given foreign key should be dropped.
*
* @param  string|array  $index
* @return \Illuminate\Support\Fluent
*/
public function dropForeign($index)
{
    return $this->dropIndexCommand('dropForeign', 'foreign', $index);
}
Enter fullscreen mode Exit fullscreen mode

Simply an abstracted call to dropIndexCommand which looks like this:

/**
* Create a new drop index command on the blueprint.
*
* @param  string  $command
* @param  string  $type
* @param  string|array  $index
* @return \Illuminate\Support\Fluent
*/
protected function dropIndexCommand($command, $type, $index)
{
    $columns = [];

    // If the given "index" is actually an array of columns, the developer means
    // to drop an index merely by specifying the columns involved without the
    // conventional name, so we will build the index name from the columns.
    if (is_array($index)) {
        $index = $this->createIndexName($type, $columns = $index);
    }

    return $this->indexCommand($command, $columns, $index);
}
Enter fullscreen mode Exit fullscreen mode

So, when we call dropForeign with an array, it builds an index name using the involved columns. So if we had a composite key index, we could drop it using the array syntax and simply pass in the column names, rather than passing in the entire index name just like we do with the foreign key constraint.

3. Model Validation

There are lots of different ways to perform user input validation in Laravel. You can use Request objects, middleware, or even validate right in your controllers. Recently, I've been defining validation rules in Eloquent Models. You can then hook into those rules wherever you need to, like in a Request object. This approach was inspried by the Elixir framework, Phoenix.

<?php

namespace Acme;

use Illuminate\Database\Eloquent\Model;

class Contact extends Model
{
    /**
     * Fillable attributes.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'phone_number',
        'user_id'
    ];

    /**
     * Validation rules for creation
     *
     * @var array
     */
    public static $createRules = [
        'name' => 'required|string',
        'phone_number' => 'string',
        'user_id' => 'integer'
    ];
}
Enter fullscreen mode Exit fullscreen mode
<?php

namespace Acme\Http\Requests;

use Acme\Contact;
use Illuminate\Foundation\Http\FormRequest;

class CreateContact extends FormRequest
{
    public function rules(): array
    {
        return Contact::$createRules;
    }
}

Enter fullscreen mode Exit fullscreen mode

You can use multiple rulesets if create and update validation differs. For instance, if your front-end only sends values that have changes for updating, you don't want all fields marked as required.

<?php

namespace Acme;

use Illuminate\Database\Eloquent\Model;

class Contact extends Model
{
    /**
     * Fillable attributes.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'phone_number',
        'user_id'
    ];

    /**
     * Validation rules for creation
     *
     * @var array
     */
    public static $createRules = [
        'name' => 'required|string',
        'phone_number' => 'string',
        'user_id' => 'integer'
    ];

    /**
     * Validation rules for update
     *
     * @var array
     */
    public static $updateRules = [
        'name' => 'string',
        'phone_number' => 'string',
        'user_id' => 'integer'
    ];
}
Enter fullscreen mode Exit fullscreen mode

4. Data Migrations

Sometimes you want to put static information in the database for reference throughout your application. Permissions, country or state names, default settings, the list goes on an on. If the data needs to be able to change or needs to be versioned, you can use migrations instead of seeders. I've tried two approaches and they both work well.

Approach #1 is to simply run queries inserting, updating, or deleting data in a migration. In this scenario I'm backfilling a new setting with a default value.

<?php

use Acme\Reports\Utilities\ReportEmailDefaults;
use Acme\Team\Team;
use Acme\Settings\Setting;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class FillEmailsSetting extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        // get all teams
        $teams = Team::all();

        foreach($teams as $team) {
            // add new default emails setting for each team
            $setting = new Setting();
            $setting->key = 'emails';

            // get values from constants to avoid magic strings
            $values = [
                'report_subject' => ReportEmailDefaults::SUBJECT,
                'report_body' => ReportEmailDefaults::BODY
            ];

            $setting->value = json_encode($values);

            $team->settings()->save($setting);
        }
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Setting::where('key', 'emails')
            ->where('settingable_type', 'team')
            ->delete();
    }
}

Enter fullscreen mode Exit fullscreen mode

Approach #2 is to extend the migration class to make it easier to add/remove data. I have used this approach when managing permissions using migrations. That might look something like this.

<?php
namespace Acme\Core\Application;

use Acme\Authentication\Permissions\Permission;
use \Illuminate\Database\Migrations\Migration;

abstract class PermissionMigration extends Migration {

    protected $permissions;

    public function getPermissions(): array
    {
        return $this->permissions;
    }

    final public function up(): void
    {
        $permissions = $this->getPermissions();

        foreach($permissions as $permission)
        {
            Permission::create($permission);
        }

    }

    final public function down(): void
    {
        Permission::whereIn('key', $this->getPermissions())->delete();
    }
}
Enter fullscreen mode Exit fullscreen mode

Whenever you want to add permissions you just need to create a new migration that extends the permission migration and define an array of permission keys to be added.

<?php

use Acme\Authentication\Permissions\CreatePost;
use Acme\Core\Application\PermissionMigration;

class AddCreatePostPermission extends PermissionMigration
{
    protected $permissions = [
        CreatePost::$key
    ];
}
Enter fullscreen mode Exit fullscreen mode

This is an incomplete example, you'd want to build ways to update and remove permissions, but I hope you get the idea.

5. Adding attributes to the request

You can add attributes to Laravel's request object using middleware. One scenario in which this is handy, is in mulit-tenant applications where you want to fetch a given team or organization and make sure the requesting user has access to it, as well as passing along the entity so you don't have to look it up multiple times.

Here is what the middleware might look like. If you are copying this at home, be aware that the $user->cannot() method is a custom helper method
I usually add to my User model. I also use a role-based permission system, but you can use Laravel Gate's and other built-in authorization
methods.

<?php

namespace Acme\Core\Http\Middleware;

use Closure;
use Acme\Authentication\Permissions\Team\ViewTeam;
use Acme\Team\Repositories\TeamRepository;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;

class LookupTeam
{
    private $teamRepository;

    public function __construct(TeamRepository $teamRepository)
    {
        $this->teamRepository = $teamRepository;
    }

    public function handle($request, Closure $next, ?string $guard = null): Closure
    {
        // Fetch the requesting user
        /* @var $user \Acme\Authentication\User */
        $user = Auth::guard('api')->user();

        $slug = $request->teamSlug;

        // Fetch the requested team
        $team = $this->teamRepository->find($slug, 'slug');

        // If no team is found, return a 404
        if ( ! $team) {
            abort(404);
        }

        // Make sure user has access
        if($user->cannot(ViewTeam::key(), $team)) {
            return response('Unauthorized', 401);
        }

        // Finally, add the team to the request
        $request->attributes->add(['team' => $team]);

        return $next($request);
    }
}

Enter fullscreen mode Exit fullscreen mode

Make sure to add the middleware to the route middleware group in your Kernel.php file, then you can attach the middleware to your route groups

Route::prefix('/team/{teamSlug}')->middleware('team-lookup')->group(function () {
    Route::get('/tasks', [TaskController:class, 'get']);
});
Enter fullscreen mode Exit fullscreen mode

And in the controller method, you can retrieve the Team from the request object.


public function get(Request $request): TaskResource
{
    $team = $request->attributes->get('team');

    $tasks = dispatch_now(new GetTeamTasks($team));

    return TaskResource::collection($tasks);
}
Enter fullscreen mode Exit fullscreen mode

6. Ordering models by distant many-to-many related items

Let's say you're building a system that tracks and manages production chains. When assigning products to machines, you want
to get a list of available machines ordered by what products they support, with the most compatible at the top of the list.

You might have a Machine entity that belongs to a Machine Type. The Machine Type entity has a Many-to-Many relationship with
Products.

So you need to do a count and order based on a many-to-many relationship with the assigned Machine Type. You can use withCount with constraints
to order the machine results based on whether or not the machine's type relationship has the assigned products you're looking for.

$machines = $machines->withCount(['type' => function($query) use($product) {
    $query->whereHas('products', function($query) use($product) {
        $query->where('products.id', $product->id);
    });
}])->orderBy('type_count', 'desc');
Enter fullscreen mode Exit fullscreen mode

Whether or not the assigned type has the assigned products will set the type_count to a 0 or a 1, which you can then order the results by.

Summary

I hope you found some of these tips useful. Feel free to add your thoughts, comments, criticisms, or ideas below!

The post 6 Laravel Tips appeared first on Matt Does Code.

Top comments (0)