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');
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'
]);
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'])
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);
}
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);
}
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'
];
}
<?php
namespace Acme\Http\Requests;
use Acme\Contact;
use Illuminate\Foundation\Http\FormRequest;
class CreateContact extends FormRequest
{
public function rules(): array
{
return Contact::$createRules;
}
}
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'
];
}
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();
}
}
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();
}
}
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
];
}
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);
}
}
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']);
});
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);
}
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');
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)