Yazan Qwaider

بعض النصائح والاستخدامات في Laravel

7 دقيقة - وقت القراءة
تاريخ النشر : قبل ١٠ أشهر

في هذا المقال سوف نتحدث عن أفضل الممارسات التي من الجيد استخدامها في Laravel, وlaravel هو php framework مفتوح المصدر (open source), ويُستخدم من قبل الكثير من المطورين حول العالم.


Eloquent

هو ORM أي Object Relational Mapper مبني على Active Record Pattern, من خلاله تستطيع التعامل مع Database بكل أريحية وسهولة في الإستخدام فهي تقدم الكثير من الميزات الجميلة التي سنتحدث عن بعضها هنا.


Exists and doesntExist

إذا أردت التحقق إذا ما كان هناك قيمة موجودة في جدول معين في قاعدة البيانات, على سبيل المثال تريد معرفة إذا تم إضافة كتب جديدة اليوم, يتم ذلك من خلال exists :

plain
Book::where(‘created_at’, today())->exists();

وعلى العكس تماماً, يمكنك التحقق إذا لم يكن هناك كتب جديدة مثلاً, من خلال doesntExist :

plain
Book::where(‘created_at’, today())->doesntExist()

Query Clone

أحيانا قد تحتاج إلى عمل query مرتين على Database وأحدهم مختلف عن الآخر قليلاً, مثلا :

ال query 1: الحصول على عدد جميع المستخدمين الذين تم إضافتهم اليوم وتم تفعيل حساباتهم.

ال query 2: الحصول على عدد جميع المستخدمين الذين تم إضافتهم اليوم ولم يتم تفعيل حساباتهم بعد.

فهنا تظهر حاجة إلى عمل نسخ لل query حتى لا نعيد كتابته مرتين أو أكثر, ولا تقل رجاءاً أننا سوف نستخدم إشارة اليساوي equal = لأنك سوف تدور وتدور وتدور حتى تعرف أين المشكلة مثل ما حدث قديماً معي 😅, لأن ال إشارة اليساوي = تقوم بما يسمى بال shallow copy فيكون النسخ بال reference وليس بال value, فيكون الذي تم عمله في query 1 موجود بال query 2.


فالحل أن نستخدم clone, إما من query نفسها أو ك php code هكذا:

plain
$query = User::where(‘created_at’, today());
$active_users = $query->clone()->where(‘is_active’, true)->count();
$unactive_users = $query->clone()->where(‘is_active’, false)->count();

أو هكذا :

plain
$active_users = (clone $query)->where(‘is_active’, true)->count();
$unactive_users = (clone $query)->where(‘is_active’, false)->count();

Accessor Cache 

ال computed هو عمل تأثير معين على column عند استرجاعه من DB ومن ثم المناداة عليه, وأحيانا قد يكون ال computed attributed مستخدم بكثرة في الكود ولا تحتاج في كل مرة تقوم مناداته أن يعيد تنفيذ callback function للحصول على النتيجة المطلوبة, فلذلك نريد حفظ القيمة الراجعة منه في ال cache من خلال shouldCache :

plain
function someAttribute() : Attribute
{
return Attribute::make(
fn () => /* Do something. */
)->shouldCache();
}

Relations in where

من المعلوم أن eloquent تقدم الكثير من طرق البحث عن البيانات المخزنة, مثل where, whereDate, whereBetween, والكثير الكثير, ولكن الذي نحن بصدد الحديث عنه هو ال where مع relation :

إذا أردت الحصول على كل posts التي لديها comments (فقط بهذه البساطة) :

plain
Post::has(‘comments’)->get();

أما إذا كنت تريد أن تقول: الحصول على كل posts التي لديها comments حديثة (اليوم) مثلا :

plain
Post::whereHas(‘comments’, function ($query) {
$query->where('published_at', today());
})->get();

أليس جميلاً, ماذا لو أخبرتك أن هناك ايضاً اختصاراً لهذا, قم باستخدام whereRelation :

plain
Post::whereRelation(‘comments’, 'published_at', today())->get();

Multiple Where Conditions

وإردافاً على ما سبق من ميزات الـ where أنها تقدم طريقة لوضع عدة conditions بشكل array, فمثلاً لو أردت البحث عن المستخدمين الذين حالة حسابهم فعّال, ولكنهم غير مشتركين :

plain
$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();

Replicate

في بعض الأحيان قد تحتاج إلى إنشاء نسخة من بيانات معينة موجود فعلا في DB, مثلا: إنشاء إعلان مشابه لإعلان قمت بإنشائه قديماً فلا تريد إعادة إدخال البيانات من جديد, ولكن قد تريد فقط تغيير العميل, وهذا الذي تقوم به replicate, ولا أنسى شعور الفرح عندما استخدمتها لأول مرة🤭, لا أدري ما السبب لكنها مفيدة :

plain
$similar_ad = Ad::where(‘topic’, ‘reading’)->first();
$new_ad = $similar_ad->replicate();
$new_ad->client_id = 1;
$new_ad->save();

Is Method

يمكنك التحقق من two model instances اذا كانوا لنفس ال row في DB أو لا من خلال is, مثلا يمكنك التحقق اذا كان ال user الزائر لصفحة منتج معين هو نفسه صاحب المنتج, هكذا :

plain
$product = Product::find(1);
$product_owner = $product->user;
if($product_owner->is(auth()->user())) {
// some code
}

Find

الكثير منا يعلم أن find تقوم بجلب عنصر محدد من خلال id مباشرة, ولكنها ايضاً بإمكانها جلب عدة عناصر من خلال عدة ids هكذا :

plain
Post::find([1, 2, 3]);

والجميل أنه يمكنك تحديد ما هي columns التي يقوم بجلبها من خلال وضع array في parameter الثاني :

plain
$posts = Post::find(1, [‘user_id’, 'status']);

ومن الجيد دائما عدم إظهار شاشة error للمستخدم عند البحث عن عنصر معين وهو في الحقيقة غير موجود, فتقع في مشكلة Access property on null, فيمكنك من خلال findOrFail إظهار شاشة NOT FOUND إذا لم يتم الحصول على العنصر وذلك من خلال ModelNotFoundException, وذلك بدلا من حدوث أمر أنت عن غنى عنه.

plain
$user = User::findOrFail($id);

Sorting

من المعروف أنه إذا أردت ترتيب العناصر الراجعة من database أن تستخدم orderBy عندما يكون ال column الذي تريد الترتيب على أساسه موجود بالأساس في table, ولكن ماذا لو أردت الترتيب على أساس mutator, فهنا قم باستخدام sortBy بدلا من orderBy :

plain
$sorted_users = User::get()->sortBy('full_name');

وماذا لو أردت الترتيب على أساس field موجود في relationship فاستخدم ال dot بهذه الطريقة :

plain
$authors = Author::with('latestBook')->get()->sortByDesc('latestBook.published_at);

withDefault

قد تواجه أحياناً أنه عند بناء ال relationship بين two models تقوم ال relation بإرجاع null بسبب عدم وجود قيمة لها في database بسبب حذفها أو لأي سبب آخر, فيمكنك بدلاً من التحقق في كل مكان تستخدمه بها أن تجعله اذا كانت قيمته null أن ينشأ له قيمة افتراضية, مثلا تريد إرجاع الـ user الذي قام بإنشاء ال post وهذا ال user غير موجود, فيمكنك باستخدام withDefault إرجاع قيمة أولية لهذا user يحمل الـ attributes أساسية :

plain
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}

ويمكنك وضع قيم معينة في الـ attributes للقيمة الافتراضية هكذا :

plain
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}

Single Action Controller

قد تحتاج في بعض الأحيان إلى بناء route يؤدي وظيفة معينة ولكن ال controllers الموجودة في مشروعك ليس لها علاقة معه بشكل أساسي, أو أن ال action معقدة فتريد فصلها في controller خاص بها, فالحل هو Single Action Controller :

نقوم بإنشاء Single Action Controller من خلال إضافة --invokable :

php artisan make:controller ProvisionServer --invokable

ستجد دالة __invoke والتي هي عبارة عن magic method, والتي يتم تنفيذها مباشرة عند عمل instance من ال class.

أما في ال route فيكون هكذا شكله :

Route::post('/server', ProvisionServer::class);


Eager loading

ال eager loading تحل مشكلة n+1 query, ولنفهم المسألة أكثر, لنفترض أن لدينا 10 مستخدمين users, وفي ال User Model علاقة مع ال Post Model فكل user لديه مقال أو أكثر, وأردت عرض المستخدمين مع عناوين المقالات الخاصة بهم, فإذا كتبت ال query بهذا الشكل :

plain
// in controller
$users = User::all();

plain
// in view
@foreach($users as $user) 
<h2>{{ $user->full_name }}</h2>

@foreach($user->posts as $post) 
<p>{{ $post->title }}</p>
@endforeach
@endforeach

فإنه إن كان لديك 10 مستخدمين على سبيل المثال, فإن عدد ال queries هي 11, ولماذا تتعب ال Engine معك يا صديقي, قل له من البداية أنك تريد ال users مع مقالاتهم وسيلبّي أمرك بكل سعادة.


plain
$users = User::with('posts')->get();

الآن عدد ال queries سيكون 2.

حتى مع عدد المقالات إن أردت العدد فقط وليس إظهار تفاصيل ال posts, فهناك withCount تنتظر الخدمة :

plain
$users = User::withCount('posts')->get();

الآن تستطيع الوصول إلى عدد المقالات من خلال posts_count.

plain
@foreach($users as $user) 
<h2>{{ $user->full_name }}</h2>
<h2>{{ $user->posts_count }} Posts</h2>
@endforeach

 و posts هنا مثالاً, أي ضع مكان كلمة ال {relation} إسم ال relation الخاص بك :

{relation}_count


DB Select

عند عمل query على table معين في حالته الافتراضية سيجلب لك كل الأعمدة في table, وقد تكون انت فقط تحتاج بعضهم وليس الكل, فلهذا قد تحتاج أن تطلب فقط الذي تحتاجه وخاصة اذا كان عدد الأعمدة كثير :


plain
// In Eloquent
User::select(‘id’, ‘email’)->get();

// In Query Builder
DB::table(‘users’)->select(‘id’, ‘email’)->get();

فإن هذا سوف يساعدة في تحسين ال performance.


DB Transaction

عند عمل مجموعة queries على DB, مثل: عند حذف user معين يقوم بحذف مقالاته وتعليقاته وكل أمر يتعلق به, فمن الممكن أن يحصل Exception لسبب ما بعد عمل query أو أكثر فلهذا لا بد من التراجع rollback عن كل التعديلات التي حدثت على DB وإظهار الخطأ للمستخدم :

plain
\DB::transaction();

try {
$user->posts()->delete();
$user->delete();
\DB::commit();
}

catch(\Exception $exception) {
\DB::rollback();
}

ال commit لإتمام العمليات التي حدثت, وال rollback التراجع عنها, فهكذا تحاول أن تجعل قاعدة البيانات consistent.


Unit Testing 

ال unit testing من أهم الأمور التي يجب عليك العمل بها وإتقانها وإستخدامها في مشاريعك, وليست مقتصرة على laravel فحسب, بل على كل اللغات أو frameworks, ولا أقصد هنا استخدام PHPUnit في python مثلاً, ولكن أقصد المفهوم نفسه وهو ال Testing, لأنه هذا سيجعل هناك إمكانية كبيرة لاكتشاف الأخطاء في الكود, وفي وقت مبكر, وخاصةً في المشاريع المتوسطة والكبيرة, فيكون من الواجب استخدامها.

ولن أخوض في شرح ال unit tests وطريقة التعامل معها في laravel, ولكن عن أمر واحد فقط.

من المفاهيم في unit test هي جعل كل unit مستقل لوحده عن بقية ال tests وأن لا يعتمد على test آخر, وأن يقوم بعمل ويختبر ال feature المطلوب اختبارها كأنه لوحده في الميدان, فلهذا من المهم عمل refresh ل database بعد كل test يقوم بالتعديل على database, فيمكنك استخدام RefreshDatabase trait في test class هكذا :

plain
class UserTest extends TestCase {
    use RefreshDatabase;

}

ولكنه يقوم بعمل refresh database بعد كل test حتى لو لم يتم التعديل على database ولهذا قم باستخدام LazilyRefreshDatabase والذي يقوم بعمل refresh فقط عندما يتم التعديل على database.


الخاتمة

هذه بعض النقاط للإستخدمات الجميلة في laravel, والباقي أكثر, ف laravel framework كثيرة التعديلات والإضافات المهمة والجميلة, فدائماً ما يكون هناك جديد عليك تعلمه فيها.

آمل أن يكون هذا المقال مفيداً لك, وأتمنى أن لا تقتصر على بعض النقاط والملاحظات من هنا وهناك, بل شمّر عن ساقيك وخُض لجة ال documentaion ففيه من الجواهر ما يجعل عينيك تبرق فرحاً 🥳.