اذهب إلى المحتوى

سمير عبود

الأعضاء
  • المساهمات

    3558
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • عدد الأيام التي تصدر بها

    34

كل منشورات العضو سمير عبود

  1. مرحباً @Mohamed Elgaml لا أدري ما المُشكلة التي تواجهها في فعل المطلوب لأن الأمر بسيط للغاية: بما أن الشريحة التي تحتوي على القيمة 3500 يتم إدخالها من طرفك فلا يوجد أي تعديل او دوال فيها لنفترض أنك وضعتها في الخانة H4 بمعنى العمود H و الصف الرابع. بالتالي H5 التي ستعرض بها الدخل السنوي ستكون تساوي H4 * 12 و هي الشريحة الثانية لديك. أما الشريحة الثالثة ستكون في الخانة H6 مثلاً و ستحتوي على H5 - 24000 بما أن القيمة 24000 قيمة ثابتة. الشريحة الرابعة ستكون قيمتها ثابتة في هذه الحالة لأن كل من القيمتين 15000 و 2,5% ثابتتين و ستحتوي القيمة 375 في كل الأحوال. ستكون القيمة ضمن الخانة H7 الشريحة الخامسة ستكون قيمتها متغيرة حسب قيمة H6 و سنضعها في الخانة H8 و ستحتوي على: (H6 - 15000) * 10% بخصوص الشريحة الأخيرة أنت تقول أنها تحتوي المبلغ النهائي لكن لا أدري ماذا تقصد بذلك لكن إذا فهمت الأمر فهو بسيط يُمكنك القيام بكل سهولة. هذا ملف يحتوي على كافة الشرائح ما عدا الشريحة الأخيرة لأني لا أعلم ماذا سيكون محتواها: Simple.xlsx
  2. إطار العمل لارافيل مُجهز بشكل جيد للقيام بالإختبارات بشتى أنواعها يُمكنك إستخدام قاعدة بيانات ثانية للقيام بالإختبارات عليها لكن أي مشروع تقوم بإنشائه ستجده مُجهز مُسبقاً للقيام بالإختبارات على قاعدة بيانات من نوع sqlite في الذاكرة أي لست بحاجة لإنشاء قاعدة بيانات بالأساس فكما تعلم أن التعامل مع الذاكرة يكون أسرع من التعامل مع ملف في القرص الصلب لذلك تحتاج فقط للذهاب إلى ملف phpunit.xml حيث ستجد السطرين التاليين: <!-- <server name="DB_CONNECTION" value="sqlite"/> --> <!-- <server name="DB_DATABASE" value=":memory:"/> --> مٌعلقين فقط قم بإزالة التعليق: <server name="DB_CONNECTION" value="sqlite"/> <server name="DB_DATABASE" value=":memory:"/> لكن قبل أن تُفكر في قاعدة البيانات يجب عليك كتابة الخطوات التي تريد أن تسير عليها و تُنفذ أمر phpunit و كل مرة يظهر لك خطأ تُحاول تصليحه إلى أن تصل إلى نجاح الإختبار مثلا لنكتب إختبار خاصية إضافة كتاب: class BookTest extends TestCase { /** @test */ public function a_book_can_be_added_to_the_library() { } } ماهي خطوات إضافة كتاب؟ أليست كالتالي إرسال بيانات الكتاب إلى مسار مُعين ثم التأكد من أن الكتاب أضيف لقاعدة البيانات إذا لنكتب هذه الخطوات داخل التابع الذي أنشأناه: <?php $this->post('/books', [ 'title' => 'Book one', 'author' => 'Kamel', ]); $this->assertCount(1, Book::all()); إذا قمنا بذلك ثم نفذنا أمر phpunit سيفشل الإختبار بالطبع لكنه سيفشل في الخطوة الثانية و نحن ننتظر منه أن يفشل في الخطوة الأولى، لماذا؟ لان لارافيل إفتراضياً تقوم بمُعالجة الإستثناءات التي تُرمى لذلك يجب أن نُخبرها بأن لا تُعالج الإستثناء و بأن تقوم بعرضه و لحسن الحظ هي توفر دالة للقيام بذلك إسمها withoutExceptionHandling و دالة أخرى إسمها withExceptionHandling و هي ما يتم تنفيذه لذلك لابد من تعديلها حتى نرى فشل الإختبار: class BookTest extends TestCase { /** @test */ public function a_book_can_be_added_to_the_library() { $this->withoutExceptionHandling(); $this->post('/books', [ 'title' => 'Book one', 'author' => 'Kamel', ]); $this->assertCount(1, Book::all()); } } الآن لو قمنا بتنفيذ أمر phpunit سيفشل و سيُعطينا السبب: NotFoundHttpException: POST http://localhost/books هذا الإستثناء يعني أن المسار الذي نُحاول الوصول له غير موجود و بالتالي نفتح ملف web.php ثم نضيف المسار: Route::post('/books', [BookController::class, 'store']); بعد أن أنشأنا المسار نُعيد تجربة الإختبار و في هذه المرة سيظهر الخطأ التالي: BindingResolutionException: Target class [BookController] does not exist. ما يعني أن المُتحكم الذي أنشأناه غير موجود. فلنقم بإنشائه: php artisan make:controller BookController ثم نقوم بإستدعائه داخل ملف web.php: <?php use App\Http\Controllers\BookController; بعد ذلك نُعيد تجربة الإختبار عن طريق الأمر phpunit و في هذه الحالة سيفشل الإختبار بالسبب التالي: BadMethodCallException: Method App\Http\Controllers\BookController::store does not exist. يعني أن التابع store غير موجود في المُتحكم BookController دعنا نضيفه: <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class BookController extends Controller { public function store() { } } مرة أخرى نُجرب الأمر phpunit و سيفشل الإختبار لكن في هذه المرة سيفشل في الخطوة الثانية و سيُعطي السبب التالي: Class 'Tests\Feature\Book' not found أي أن الصنف Book غير موجود دعنا نقوم بإضافة النموذج و ملف التهجير الخاص به و نستدعيه في كلاس الإختبار فوق: php artisan make:model Book -m وفي الكلاس BookTest نقوم بإستدعائه: use App\Models\Book; مرة أخرى نُجرب الأمر phpunit و بطبيعة الحال سيفشل أيضا و سيُعطي السبب التالي: QueryException: SQLSTATE[HY000]: General error: 1 no such table: books (SQL: select * from "books") هذا معناه أنه يُحاول جلب كل الكتب لكن نحن لغاية الآن لم نقم بتهجير قاعدة البيانات و نرجع لأول ما أخبرتك به أن إستعمال قاعدة بيانات ثانية من أجل الإختبارات لذلك نقوم بإلغاء التعليق من السطرين في ملف phpunit.xml، ثم نضيف ال trait الذي إسمه RefreshDatabase داخل صنف الإختبار ستجد أنه قد تم إستدعاؤه لكنه غير مستخدم دعنا نستخدمه: use Illuminate\Foundation\Testing\RefreshDatabase; class BookTest extends TestCase { use RefreshDatabase; } و هذا ال trait الهدف منه هو تنفيذ أمر التهجير عند تنفيذ الإختبار و عند إنتهاء الإختبار يقوم بعمل reset. بعد ذلك نقوم مرة أخرى بتنفيذ الأمر phpunit و بطبيعة الحال سيفشل أيضا و سيُعطي السبب التالي: Failed asserting that actual size 0 matches expected size 1. هذا الخطأ يعني أنه لم يتم إضافة الكتاب فهو جلب كل الكتب لكن وجد أن عددها 0 لكن نحن توقعنا 1 و هذا لأن الدالة store لا تقوم بشيء لغاية الآن، دعنا نضيف أكواد إضافة كتاب: use App\Models\Book; class BookController extends Controller { public function store() { Book::create([ 'title' => request('title'), 'author' => request('author') ]); } } بعد ذلك نقوم مرة أخرى بتنفيذ الأمر phpunit و بطبيعة الحال سيفشل لكن هذه المرة سيُعطي الخطأ التالي: MassAssignmentException: Add [title] to fillable property to allow mass assignment و هذا لأن لارافيل تُحاول حمايتك يُمكننا في هذه الحالة إخبار لارافيل أننا سنقوم بحماية أنفسنا و لا داعي أن تتدخل في هذا الأمر بعمل set للخاصية guarded في النموذج Book و جعلها فارغة: class Book extends Model { use HasFactory; protected $guarded = []; } بعد ذلك نقوم مرة أخرى بتنفيذ الأمر phpunit و بطبيعة الحال سيفشل أيضاً بسبب أن ملف التهجير الخاص بالكتب لا يوجد به الأعمدة title و author: QueryException: SQLSTATE[HY000]: General error: 1 table books has no column named title نقوم بإضافتها في ملف التهجير: $table->string('title'); $table->string('author'); ثم نعيد تجربة الإختبار عن طريق الأمر phpunit و أخيراً هذه المرة سينجح الإختبار. بسبب أن خاصية إضافة كتاب تم إضافتها بنجاح. و هكذا تقوم بإتباع نفس الآلية في بقية الإختبارات، نحن لم نختبر ال validation و لم نختبر إعادة توجيه المُستخدم مثلاً ، يُمكنك إختبار الخصائص بنفس الطريقة
  3. البرمجة الموجهة بالإختبار هي البرمجة عن طريق البدء بكتابة إختبارات لكل وظيفة بالتطبيق، وكل إختبار يجب أن يقوم بتحديد وشرح الكيفية التي سيتم التعامل بها مع الكود وما هو الناتج الذي من المفترض أن يقوم بإرجاعه في حال تم تنفيذ كل شيء بنجاح، وتقوم هذه الاختبارات بإرجاع إما حالة نجاح والتي تعني أن الكود يعمل بالطريقة الصحيحة التي يجب أن يقوم بها، أو حالة فشل في حال أن الكود به أخطاء وأنه لا يقوم بإرجاع الناتج الصحيح الذي يجب أن يقوم بإرجاعه لحالة الاختبار تلك. لذلك إن كنت تريد إنشاء تطبيق أو مشروع يعمل بشكل جيد و مُختبر من عدة نواحي، و تنتقل بمستواك في تطوير البرمجيات إلى مُستوى أعلى فعليك النظر في هذا الموضوع. هذه بعض الفوائد: تحدد لك الطريق للبدء بكتابة الكود فعوض أن تكون متردد في كتابة خصائص التطبيق وقلق بشأن إذا ما كان الكود الذي تكتبه سيعمل بشكل صحيح فالإختبارات ستقوم بقيادتك تقليل الأخطاء في المشروع يعطيك الثقة بالنفس تسهيل عملية إعادة بناء التعليمات البرمجية أو ما يُسمى بال refactoring دون القلق على عمل الخصائص إضافة خصائص و العمل على طلبات العميل بشكل سليم و بدون أية قلق هناك بعض القواعد التي يجب أن تنتبه لها عند إنشائك لأصناف الإختبار في laravel ف phpunit تبحث عن الملفات التي تنتهي ب Test.php لذلك على كل من الملف و الكلاس أن ينتهيان ب Test. و عند إنشائك لملف Test جديد عن طريق أمر Artisan عليك أن تُحدد ذلك: php artisan make:test JustExampleTest أيضاً أسماء التوابع يجب أن تبدأ بالكلمة test كما يلي: public function test_method_to_assert_something() { $result = 2 * 5; $this->assertEquals(50, $result); } الآن إذا قمت بتنفيذ أمر phpunit سيفشل الإختبار و هو ما نتوقعه ان يحدث. وسيُعطيك النتيجة التالية: Failed asserting that 10 matches expected 50. إذا أردنا تسمية التوابع بأسماء أخرى غير التي تبدأ ب test يُمكننا ذلك لكن يجب أن نُحدد ال annotation لكل تابع على أنه test بهذا الشكل مثلا: class JustExampleTest extends TestCase { /** @test */ public function it_just_a_simple_test() { $result = 2 * 5; $this->assertEquals(50, $result); } } بعض هذه الإعدادات يُمكن تعديلها من خلال ملف phpunit.xml لكن لا داعي لذلك، فقط إحترم تسمية الملفات و ضع ال annotation.
  4. يُمكنك حذف جميع ملفات مُجلد بإستخدام التابع cleanDirectory على الواجهة: File بهذا الشكل: File::cleanDirectory("storage/app/public/products"); و سيُعطيك true في حالة تم الحذف أما إن كان المسار غير موجود سيُعطيك false. او بإمكانك إستخدام التالي أيضاً: use Illuminate\Filesystem\Filesystem; $fs = new Filesystem; $fs->cleanDirectory("storage/app/public/products"); أما إن أردت حذف مُجلد بكافة الملفات التي بداخله يُمكنك إستخدام التابع deleteDirectory بهذا الشكل: File::deleteDirectory("storage/app/public/products"); الفرق بين الطريقتين ان الأولى تحذف الملفات التي بداخل المُجلد فقط و تُبقي على المُجلد نفسه أما الثانية فتحذف المُجلد نفسه و ما بداخله. أما الطريقة التي تستخدمها فهي لحذف ملف واحد فقط بتمرير مساره للتابع delete كما يمكنك إستخدام نفس التابع لحذف مجموعة من الملفات بتمرير مسارات هذه الملفات في مصفوفة و بالتالي يُمكن إستخدام التابع allFiles لجلب الملفات الموجودة في مجلد ما على شكل مصفوفة و نُمررها للتابع delete بالشكل التالي: $files = Storage::allFiles("public/products"); Storage::delete($files); لاحظ أننا لم نمرر المسار بداية من app لأنه توابع الصنف Storage تنظر مباشرة داخل المجلد app و بالتالي إن قمت بتمريره في مسار الملف ستفترض أن هناك مجلد app داخل مُجلد app هكذا: storage/app/app
  5. ليس هناك فترة محددة فبمجرد إشتراكك بأحد الدورات تستطيع الوصول إلى محتوياتها مدى الحياة اي أنك لن تكون مقيد بوقت أو تاريخ محدد لإنهائها. لذلك فقضية إكمال الدورة راجعة لك كشخص و طبيعة دراستك و فهمك. أهم شيء أن تركز على فهم الدروس بشكل جيد و لا تقيد نفسك بوقت أو تاريخ محدد
  6. يُمكن أن تأخذ الإجراءات بعض الوقت لذلك لا تقلق سيتم إعلامك مباشرة بعد الموافقة، إنتظر قليلاً لمدة أقصاها 24 ساعة و إن لم يتم التواصل معك حينها يُمكنك مراسلة الدعم الفني كما أخبرتك.
  7. مرحباً @Ahmed Ali51 إذا كنت قد إشتركت في إحدى الدورات فإنه سيتم إرسال بريد إلكتروني لك للعنوان الذي اشتركت به يحتوي على كافة الإرشادات و الخطوات اللازمة للإلتحاق بالدورة التي اشتركت فيها. إذا لم يصلك أي بريد تأكد إن كنت لا تستطيع الوصول للدورة التي التحقت به من خلال القائمة العلوية: ثم الضغط على دوراتي، إذا لم يظهر لك ذلك يُمكنك مراسلة الدعم الفني من خلال: مركز المساعدة ثم فتح تذكرة و شرح المُشكلة لهم و سيتواصلون معك في أقرب وقت.
  8. هناك طريقتين للتعامل مع قاعدة البيانات إما عن طريق ال eloquent orm و هي نفس الطريقة التي قد إستخدمناها في الدورات او عن طريق ال query builder. ال orm هو رابط الكائنات بالعلاقات و يعتمد بشكل أساسي على نماذج و الربط بينها عن طريق العلاقات وكل نموذج يتم ربطه مع جدول في قاعدة البيانات أما ال query builder او مُنشئ الإستعلامات و هي نفس الطريقة التي ذكرتها و نستخدم فيها ال DB facade و هي عبارة عن واجهة تسهل التعامل مع قاعدة البيانات بشكل سلس. كلا الطريقتين تؤديان نفس الغرض، و إن كانت طريقة مُنشئ الإستعلامات أسرع بنسبة صغيرة و افضل من ناحية الأداء من رابط الكائنات بالعلاقات في بعض الأحيان إلا أن مُعظم مُطوري لارافيل يستخدمون eloquent لكون الشيفرات التي يتم كتابتها به مقروءة بشكل كبير، إن كان المُطور سيعتمد بشكل كلي على query builder و لا يستعمل النماذج (models) نهائياً فإستعماله للإطار و لمفهوم MVC خاطئ خصوصا إن كان المشروع كبير و العلاقات الموجوة بين الجداول كثيرة، فإستعمال الquery builder لا يعني بالضرورة الإستغناء عن النماذج (models). و إستعمال الquery builder لا يعني بالضرورة أيضاً عدم إستخدام eloquent و العكس صحيح. شخصياً استخدم eloquent و أحياناً عندما يكون الإستعلام مُعقد قليلاً و كمية البيانات التي يتم معالجتها كبير جداً في عمليات البحث و الفلترة او جلب تقارير من بيانات ضخمة في هذه الحالة الأفضل إستخدام Query Builder.
  9. من بين المشاريع التي يُمكنك التدرب و تطبيق الأساسيات عليها هي مشروع بناء منتدى بسيط حيث يُمكن مبدئياً العمل على هذه الخصائص: في المنتدى مجموعة من الأعضاء يُمكنهم تسجيل الدخول و الخروج و الإشتراك إتاحة إنشاء مُساهمات داخل المُنتدى للأعضاء الردود على المُساهمات تصنيفات المُساهمات فلترة المُساهمات حسب التصنيفات فلترة المُساهمات حسب الأكثر شعبية و التي لم يتم الإجابة عنها. فلترة المُساهمات حسب المستخدمين إمكانية الإعجاب بالردود الحساب الشخصي لكل عضو عرض نشاط المستخدم في المنتدى إمكانية الإشتراك بمُساهمة بحيث عندما يتم إضافة رد عليها يتم إشعار المُشتركين بذلك و العديد من الخصائص التي بإمكانك إضافتها مع الوقت
  10. قد تحدث المُشكلة بسبب أحد النقاط التالية: أنك تقوم بتصفح الرابط بشكل خاطئ أي أن المسار الذي تُحاول الوصول إليه فعلاً غير موجود. فيجب التأكد من هذه النُقطة أولاً أن المسار الذي تحاول الوصول إليه مُتضارب مع مسار آخر موجود فوقه في الترتيب فإن كان لديك مسار آخر بالشكل التالي: Route::get('/threads/{thread}', [ThreadController::class, 'show']); Route::get('/threads/create', [ThreadController::class, 'create']); و كنت تُحاول الوصول إلى الرابط threads/create/ فلن يتم أخذ المسار الثاني بعين الإعتبار بل سيتم في هذه الحالة تنفيذ المسار الأول و تمرير الكلمة create كمُعامل للتابع show. لذلك إن أردت أن يكون المسار الخاص بالتابع create له اولوية ضعه فوق المسار الذي به مُعامل بهذا الشكل: Route::get('/threads/create', [ThreadController::class, 'create']); Route::get('/threads/{thread}', [ThreadController::class, 'show']); أن التطبيق الخاص بك يعمل بالcache أي أن المسارات مُخزنة في الcache فأي مسار جديد تقوم بإضافته لا يتم الإنتباه له و في هذه الحالة يجب عليك عمل clear لل cache الخاص بالمسارات بتنفيذ أحد هذه الأوامر: php artisan route:clear // او php artisan optimize:clear حيث أن الأمر الأخير يقوم بعمل clear لعدة أشياء ليس المسارات فحسب بل صفحات العرض و الإعدادات وما إلى ذلك.
  11. Mass Assignment او التعيين الجماعي هو عندما تقوم بإرسال مصفوفة بمفاتيح و قيم مُتنوعة للعملية إنشاء كائن ما سواء عن طريق الباني أو عن طريق دوال eloquent مثل create او update: <?php $model = new Model([ 'key1' => 'value1', 'key2' => 'value2', . . . ]); // او $model = Model::create([ 'key1' => 'value1', 'key2' => 'value2', . . . ]); // او $model->update([ 'key1' => 'value1', 'key2' => 'value2', . . . ]); في هذه الحالة لارافيل ستُحاول حمايتك تلقائياً من بعض الثغرات التي تُسببها هذه العملية. قد يظهر لك الأمر عادي لكن هذه العملية تحميك كثيرا مثلاً إذا كان لديك جدول للمُستخدمين و به عمود is_admin إذا كانت قيمته 1 فإن المُستخدم سيكون مُشرف في التطبيق، قد يستغل اي شخص ذلك و يُرسل حقل مخفي is_admin مع الطلب قيمته 1 و بالتالي سيُصبح مشرف بدل أن يكون مستخدم عادي. توفر laravel طريقة للحماية من هذا الأمر فيُمكنك مثلا إستخدام الطريقة العادية في إنشاء كائن جديد بتعيين الخصائص بشكل منفصل: <?php $model = new Model(); $model->prop1 = "value1"; $model->prop2 = "value2"; . . . $model->save(); لكن إن أردت إستخدام التعيين الجماعي يُمكنك إستخدام أحد المصفوفات fillable او guarded الموجودتان ضمن النموذج حيث في المصفوفة fillable تضع كافة الخصائص التي تريد أن تُعين جماعياً: protected $fillable = ['prop1', 'prop2']; أما المصفوفة guarded فتُستخدم لحماية بعض الخاصيات التي لا تريد أن تُعين جماعياً على سبيل المثال: protected $guarded = ['is_admin']; بطبيعة الحال يُمكنك فقط إستخدام أحد المصفوفتين فهما يعملان عكس بعضهما، ف fillable تعني القائمة البيضاء و guarded تعني القائمة السوداء أما بخصوص: غالباً ما يحدث هذا الخطأ بسبب csrf token تقوم لارافيل بحقنه في الجلسة و إرساله مع كل نموذج لذلك إن كان لديك نموذج form يجب عليك إضافة الحقل المخفي csrf سواء عن طريق: <form action="" method="post"> @csrf ... او عن طريق <form action="" method="post"> {{ csrf_field() }} ... أحيانا يكون هذا الحقل مُضمن في النموذج و يحدث الخطأ و ذلك راجع غالباً لإنتهاء الجلسة او أن الجلسة لم يتم حفظها بشكل جيد، إفتراضياً يتم حفظ الجلسات على شكل ملفات في المُجلد storage. قد لا تكون الصلاحيات المُطبقة على هذا المُجلد مناسبة مما لا يسمح بحفظ الملفات عليه.
  12. يُمكنك إنشاء أمر artisan خاص يقوم بعمل النُسخة التي تريدها عند تنفيذه و لإنشاء ذلك الأمر نقوم بدايةً بتنفيذ الأمر التالي: php artisan make:command DatabaseBackUp سيُنشئ لك الأمر صنف بالإسم DatabaseBackUp في المسار: app/Console/Commands/DatabaseBackUp تقوم بالتعديل عليه ليُصبح بالشكل التالي: <?php namespace App\Console\Commands; use Carbon\Carbon; use Illuminate\Console\Command; class DatabaseBackUp extends Command { protected $signature = 'database:backup'; protected $description = 'create database backup'; public function __construct() { parent::__construct(); } public function handle() { $filename = "backup-" . Carbon::now()->format('Y-m-d') . ".gz"; $command = "mysqldump --user=" . env('DB_USERNAME') ." --password=" . env('DB_PASSWORD') . " --host=" . env('DB_HOST') . " " . env('DB_DATABASE') . " | gzip > " . storage_path() . "/app/backup/" . $filename; $returnVar = NULL; $output = NULL; exec($command, $output, $returnVar); } } بعد ذلك تقوم بإنشاء مُجلد backup في المسار: storage/app/backup و عندما تريد إنشاء نُسخة من قاعدة البيانات فقط تقوم بتنفيذ الأمر: php artisan database:backup و هذا الأمر سيقوم بإنشاء نُسخة من قاعدة البيانات بنفس تاريخ تنفيذ الأمر. في المُجلد الذي أنشأناه.
  13. إسم الصنف هو about-me لكن عند تطبيقك للصنف في العنصر كتبت about me <!--start about me--> <div class="about me"> <div class="container"> <div class="image"> <img src="../image/300px.png" alt="test"> </div> <div class="info"> info test </div> </div> </div> <!--end about me--> هذا يعني أنك تُطبق صنفي تنسيقات على العُنصر أحدهما about و الثاني me لأنك فصلت الإثنين بمسافة. إذا كنت تريد تطبيق الصنف about-me عليك أن تكتبه بشكل صحيح: <div class="about-me"> </div>
  14. يُمكن ذلك بإضافة حقل جديد لجدول المُستخدم banned_until و إستخدام middleware تعمل على منع المُستخدم من تسجيل الدخول إذا كان في فترة توقيف حسابه إضافة الحقل banned_until إلى جدول المستخدمين: php artisan make:migration add_banned_until_to_users_table هذا الأمر سيُنشئ ملف تهجير يُمكننا من إضافة حقل جديد لجدول المستخدمين. class AddBannedUntilToUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->timestamp('banned_until')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('banned_until'); }); } } بعد ذلك نقوم بتنفيذ أمر التهجير: php artisan migrate هذا يعني أن المُستخدم إن كانت banned_until فارغة أي null فإن الحساب نشيط، اما إن كان بالحقل تاريخ مُستقبلي فإن الحساب قد تم إيقافه. نضيف الحقل إلى مصفوفة fillable و مصفوفة dates في النموذج User و ذلك حتى نستطيع مُعاملة الحقل على أنه تاريخ: class User extends Authenticatable { protected $fillable = [ 'name', 'email', 'password', 'banned_until' ]; protected $dates = [ 'banned_until' ]; } نقوم بإنشاء middleware نُسميها CheckBanned بهذا الأمر: php artisan make:middleware CheckBanned سيتم إنشاء الmiddleware في المسار التالي app/Http/Middleware نقوم بفتح الملف و نُعدل على الدالة handle بالشكل التالي: <?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Str; class CheckBanned { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle(Request $request, Closure $next) { if (auth()->check() && auth()->user()->banned_until && now()->lessThan(auth()->user()->banned_until)) { $banned_days = now()->diffInDays(auth()->user()->banned_until); auth()->logout(); $message = 'Your account has been suspended for '.$banned_days.' '.Str::plural('day', $banned_days).'. Please contact administrator.'; return redirect()->route('login')->withMessage($message); } return $next($request); } } تعمل الmiddleware على التحقق من أن المُصادقة تمت أي أن المستخدم أدخل بيانات صحيحة، و أن الحقل banned_until لهذا المُستخدم ليس فارغ و أن الحقل به تاريخ مُستقبلي: إذا تحققت هذه الشروط تقوم بحساب فارق الأيام بين التاريخ الحالي و تاريخ الإيقاف و تقوم بتسجيل خروج للمُستخدم و إعادة توجيهه إلى صفحة تسجيل الخروج مع تمرير الرسالة. يُمكن تمرير رسالة مُختلفة بحسب عدد الأيام كأن نتحقق إن كان عدد الأيام أقل من 20 نعرض عدد الأيام و إذا كان عدد الأيام اكبر لا نعرض عدد الأيام. فقط نقول أن الحساب تم إيقافه و لا نذكر المدة. الأمر راجع لك في هذه النقطة. يتبقى تسجيل هذه الmiddleware و يتم ذلك على مستوى الملف app/Http/Kernel.php في المصفوفة middlewareGroups داخل المصفوفة ذات المفتاح web: protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, // ... other middleware classes \App\Http\Middleware\CheckBanned::class, ], الآن نقوم بعرض الرسالة في حالة كانت موجودة على مستوى صفحة العرض login.blade.php: <div class="card-body"> @if (session('message')) <div class="alert alert-danger">{{ session('message') }}</div> @endif <form method="POST" action="{{ route('login') }}"> ... و هذه النتيجة:
  15. بإمكانك إستخدام أمر إنشاء ملف تهجير فملفات التهجير ليست مُخصصة فقط لإنشاء الجداول و إنما تُمكنك أيضاً من إضافة عمود جديد لجدول بيانات أيضاً. لنفترض أننا نريد إضافة عمود age للجدول users فيتم ذلك عن طريق الأمر التالي: php artisan make:migration add_age_to_users_table هذا الأمر سيُنشئ لنا ملف migration جديد و سيحتوي على الصنف التالي: class AddAgeToUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { }); } } الآن نقوم بإضافة العمود في الدالة up و من خلال الدالة down نقوم بحذفه كما هو موضح أدناه: <?php public function up() { Schema::table('users', function (Blueprint $table) { $table->unsignedTinyInteger('age')->nullable()->after('username'); }); } و هذا يعني أننا نريد إضافة العمود age على أنه unsignedTinyInteger إضافة الى أنه سيكون فارغ إفتراضياً و سيُضاف بعد العمود username. أما الدالة down ستكون بهذا الشكل: <?php public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('age'); }); } و بالتالي كل ما عليك الآن هو تنفيذ امر التهجير: php artisan migrate هذا الأمر سيُنفذ الدالة up و يُضيف العمود الجديد. أما الدالة down يتم تنفيذها عند تنفيذ أمر rollback الذي سيحذف العمود age.
  16. NullPointerException هو عبارة عن إستثناء يتم رميه عندما تقوم بتعريف متغير ولكنك لم تقم بإنشاء كائن وتعيينه إلى هذا المتغير أي أنه يكون يشير إلى Null ثم تستدعي أحد الدوال عليه. في الحالات العادية عند إنشائك لمُتغير و لا تقوم بإسناد أي قيمة له ثم تحاول إستخدام هذا المُتغير سينتبه مُترجم اللغة مثلا إن قمت بالتالي: Employee e; System.out.println(e); سيُعطيك المُترجم الخطأ التالي: variable e might not have been initialized لكن أحياناً أخرى قد تكتب كود لا يقوم بإنشاء الكائن مُباشرة بل يتم إنشاؤه بشكل غير مُباشر مثلاً: لدينا صنف الموظف التالي: class Employee{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return name.toUpperCase(); } } ثم نقوم بإنشاء كائن من الصنف Employee بهذا الشكل: Employee e = new Employee(); إلى الآن لا توجد أي مُشكلة، لكن في هذه اللحظة عند إنشائنا لكائن من النوع Employee فإن الخاصية name تُشير إلى null و بالتالي إذا ما قمنا بطباعة المُتغير e بهذا الشكل: System.out.println(e); فإنه سيتم إستدعاء الدالة toString وهذه الدالة تقوم بإرجاع الإسم بحروف كبيرة أي تستدعي الدالة toUpperCase على الخاصية name لكن كما تعلم فنحن لم نُسند للخاصية name اي قيمة فهي تُشير إلى null و هنا سيتم رمي الإستثناء NullPointerException. طرق مُعالجة الأمر كثيرة فمثلاً يُمكنك إستخدام try catch و يتم ذلك بوضع الكود الذي نشك أن فيه خلل و أنه سيُسبب في رمي الإستثناء في البلوك try و في البلوك catch نقوم بالمعالجة try { Employee e = new Employee(); System.out.println(e); } catch (NullPointerException ex) { System.out.println(ex.getMessage()); } . او نقوم بالتحقق من المُتغير إذا كان لا يُساوي null نقوم بالعملية التي نريد بهذا الشكل في مثالنا: @Override public String toString() { if (name != null) return name.toUpperCase(); return "Employee name property == null"; }
  17. لا أدري ماذا يوجد في الملف a.txt لديك لكن كما أخبرتك سابقاً يُمكن إستخدام مُتغير من نوع string تُخزن فيه مُحتوى الملف a.txt ثم تكتب ما خزنته بإستعمال الدالة fputs في الملف الذي تريد بهذا الشكل: #include <stdio.h> #include <string> using namespace std; int main () { FILE *fp; string content = "content of source file"; fp = fopen("file.txt", "w"); fputs(content.c_str(), fp); fclose(fp); return(0); }
  18. لم أفهم ماذا تريد بالضبط في هذه النقطة: الإنسان حتى ينقل محتوى ملف يقرأ من الملف الأول ثم يُخزن ما قرأ في ذاكرته و بعد ذلك يكتب ماهو موجود في ذاكرته، نفس الشيء تقريباً هنا يجب فتح الملف الأول للقراءة و الثاني للكتابة ثم كل حرف تقرأه من الملف الأول تكتبه في الملف الثاني حتى نصل للنهاية.
  19. يوجد لديك خطأ في إستعمال الدالة fputs هذه الدالة تستقبل سلسلة نصية على شكل مصفوفة محارف كمُعامل أول و مؤشر على File كمُعامل ثاني بهذا الشكل مثلاً: #include <stdio.h> int main () { FILE *fp; fp = fopen("file.txt", "w+"); fputs("This is c programming.", fp); fclose(fp); return(0); } أنت لم تقرأ شيء من الملف الذي تريد نقل مُحتواه فللكتابة في الملف الثاني يجب قراءة مُحتوى الملف الأول ثم نقله للملف الثاني بهذا الشكل مثلاً: #include <iostream> #include <stdio.h> using namespace std; int main() { FILE* pFile; // المصدر FILE* pofg; // الخرج char ch; // مُتغير سنستخدمه للقراءة pFile = fopen("a.txt", "r"); // فتح المصدر للقراءة pofg = fopen("abc.txt", "w"); // فتح الخرج للكتابة if (pFile != NULL) // إذا كان المصدر لا يُساوي null { ch = fgetc(pFile); // قراءة المحرف الأول من المصدر while(ch != EOF) // بما اننا لم نصل إلى نهاية الملف { fputc(ch, pofg); // أكتب في الخرج ما قمت بقرائته ch = fgetc(pFile); // واصل القراءة } cout<<"\nFile copied successfully."; // تم نقل المحتوى fclose(pFile); // غلق المصدر fclose(pofg); // غلق الخرج } return 0; }
  20. لنفترض أنه لديك حقل username في جدول المُستخدمين بهذا الشكل: $table->string('username')->unique(); الآن يجب أن تُعدل على نموذج تسجيل الدخول و بالضبط في حقل البريد ليُصبح بهذا الشكل: <div class="form-group row"> <label for="login" class="col-md-4 col-form-label text-md-right">{{ __('Username or Email') }}</label> <div class="col-md-6"> <input id="login" type="text" class="form-control{{ $errors->has('username') || $errors->has('email') ? ' is-invalid' : '' }}" name="login" value="{{ old('username') ?: old('email') }}" required autofocus> @if ($errors->has('username') || $errors->has('email')) <span class="invalid-feedback"> <strong>{{ $errors->first('username') ?: $errors->first('email') }}</strong> </span> @endif </div> </div> في ملف login.blade.php، و هذا حتى يقبل الحقل إدخال نص و عرض رسائل الخطأ. الآن يجب التعديل على المُتحكم LoginController سنُجري عليه بعض التعديلات حتى يُلائم الوضعية: نضيف الخاصية التالية للمُتحكم: protected $username; ثم من خلال الباني نقوم بتهيئة هذه الخاصية: public function __construct() { $this->middleware('guest')->except('logout'); $this->username = $this->findUsername(); } لاحظ أنها تأخذ قيمتها من خلال ما يُرجعه التابع findUsername لذلك نقوم بإنشائه في المتحكم أيضاً: <?php public function findUsername() { $login = request()->input('login'); // جلب قيمة النص الذي أدخله المُستخدم في الحقل $fieldType = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; // بإستخدام القيمة يُمكننا معرفة إن كان المُدخل بريد او نص و على اساسها نضع نوع الحقل request()->merge([$fieldType => $login]); // وضع حقل جديد في الطلب return $fieldType; // email or username } كل ما يفعله التابع أنه يجلب لنا email او username على حسب القيمة المُدخلة و يضيف ذلك لمصفوفة الطلب: إذا كان المُدخل بريداً: 'email' => 'قيمة الحقل' إذا لم يكن بريد إلكتروني: 'username' => 'قيمة الحقل' الآن نقوم بإعادة تعريف كل من التابع username و التابع login اللذان يستخدمها المُتحكم LoginController إعتمادًا على الTrait AuthenticatesUsers لن نُغير على التابع login كثيراً سنأخذه كما هو و نضيف بعض الأشياء فقط: التابع username: <?php public function username() { return $this->username; } التابع login: <?php public function login(Request $request) { //نُغير التحقق $this->validate($request, [ 'login' => 'required|string', 'password' => 'required|string', ]); // نجلب نوع التسجيل $login_type = filter_var($request->input('login'), FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; // نضيفه لمصفوفة الطلب $request->merge([ $login_type => $request->input('login') ]); // بقية الأشياء موجودة من الإطار نفسه و لن نغير فيها // If the class is using the ThrottlesLogins trait, we can automatically throttle // the login attempts for this application. We'll key this by the username and // the IP address of the client making these requests into this application. if (method_exists($this, 'hasTooManyLoginAttempts') && $this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); return $this->sendLockoutResponse($request); } if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request); } // If the login attempt was unsuccessful we will increment the number of attempts // to login and redirect the user back to the login form. Of course, when this // user surpasses their maximum number of attempts they will get locked out. $this->incrementLoginAttempts($request); return $this->sendFailedLoginResponse($request); } لا تنسى إستدعاء الكلاسات المُستخدم في المُتحكم. هذا مُحتوى المُتحكم LoginController بعد التعديلات المُضافة: <?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ use AuthenticatesUsers; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = RouteServiceProvider::HOME; protected $username; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest')->except('logout'); $this->username = $this->findUsername(); } public function findUsername() { $login = request()->input('login'); $fieldType = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; request()->merge([$fieldType => $login]); return $fieldType; } public function username() { return $this->username; } public function login(Request $request) { $this->validate($request, [ 'login' => 'required|string', 'password' => 'required|string', ]); $login_type = filter_var($request->input('login'), FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; $request->merge([ $login_type => $request->input('login') ]); // If the class is using the ThrottlesLogins trait, we can automatically throttle // the login attempts for this application. We'll key this by the username and // the IP address of the client making these requests into this application. if (method_exists($this, 'hasTooManyLoginAttempts') && $this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); return $this->sendLockoutResponse($request); } if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request); } // If the login attempt was unsuccessful we will increment the number of attempts // to login and redirect the user back to the login form. Of course, when this // user surpasses their maximum number of attempts they will get locked out. $this->incrementLoginAttempts($request); return $this->sendFailedLoginResponse($request); } } ملاحظة: هذه الطريقة مُستخدمة على إصدار 8 من لارافيل، بإستعمال الحزمة Laravel/ui
  21. هناك عدة حزم مُعظم المُطورون يستخدمون إحداها لعمل هذه الجزئية في مشروعهم و من هذه الحزم أذكر: Laravel-permission التي تم تطويرها من طرف spatie و هم معروفين بتطوير عدة حزم مشهورة و مُستخدمة و يُطورون عليها بإستمرار. bouncer من تطوير Joseph Silber Laratrust هذه الحزم أكثر إستخداماً في هذا الموضوع و هي تُحدث بإستمرار حتى تدعم الإصدارات الجديدة من إطار laravel. إن كنت تريد إنجاز هذا الأمر فستتعب قليلاً و هذه الخطوات يُمكنك تطبيقها للوصول إلى ما تريد: إنشاء مودل لكل من Role و Permission و ملف تهجير لكل منهما: php artisan make:model Permission -m php artisan make:model Role -m كما تعلم فتمرير -m يعني أننا نريد إنشاء ملف تهجير أيضاً. تعديل ملفات التهجير: ملف تهجير الصلاحيات: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePermissionsTable extends Migration { public function up() { Schema::create('permissions', function (Blueprint $table) { $table->id(); $table->string('name'); // edit posts $table->string('slug'); //edit-posts $table->timestamps(); }); } public function down() { Schema::dropIfExists('permissions'); } } ملف تهجير الأدوار: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateRolesTable extends Migration { public function up() { Schema::create('roles', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('slug'); $table->timestamps(); }); } public function down() { Schema::dropIfExists('roles'); } } إنشاء ملفات تهجير الجداول الإضافية لربط الصلاحيات بالمستخدمين و ربط الصلاحيات بالأدوار و ربط الأدوار بالمُستخدمين: php artisan make:migration create_users_permissions_table --create=users_permissions php artisan make:migration create_users_roles_table --create=users_roles php artisan make:migration create_roles_permissions_table --create=roles_permissions و في ما يلي مُحتوى ملفات التهجير التي أنشأناها: جدول users_permissions: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersPermissionsTable extends Migration { public function up() { Schema::create('users_permissions', function (Blueprint $table) { $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('permission_id'); //FOREIGN KEY CONSTRAINTS $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['user_id','permission_id']); }); } public function down() { Schema::dropIfExists('users_permissions'); } } جدول users_roles: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersRolesTable extends Migration { public function up() { Schema::create('users_roles', function (Blueprint $table) { $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('role_id'); //FOREIGN KEY CONSTRAINTS $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['user_id','role_id']); }); } public function down() { Schema::dropIfExists('users_roles'); } } جدول roles_permissions: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateRolesPermissionsTable extends Migration { public function up() { Schema::create('roles_permissions', function (Blueprint $table) { $table->unsignedBigInteger('role_id'); $table->unsignedBigInteger('permission_id'); //FOREIGN KEY CONSTRAINTS $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['role_id','permission_id']); }); } public function down() { Schema::dropIfExists('roles_permissions'); } } نقوم بعملية التهجير: php artisan migrate إنشاء العلاقات بين النماذج علاقات النموذج Role: public function permissions() { return $this->belongsToMany(Permission::class,'roles_permissions'); } public function users() { return $this->belongsToMany(User::class,'users_roles'); } علاقات النموذج Permission: public function roles() { return $this->belongsToMany(Role::class,'roles_permissions'); } public function users() { return $this->belongsToMany(User::class,'users_permissions'); } إنشاء Trait لإستخدامه في النموذج User، في مٌجلد app نقوم بإنشاء مُجلد Permissions و نضع بداخله ملف HasPermissionsTrait.php ثم نقوم بتضمينه داخل النموذج User: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use App\Permissions\HasPermissionsTrait; class User extends Authenticatable { use HasFactory, Notifiable, HasPermissionsTrait; } يكون مُحتوى الملف HasPermissionsTrait بالشكل التالي: <?php namespace App\Permissions; use App\Models\Permission; use App\Models\Role; trait HasPermissionsTrait { public function givePermissionsTo(... $permissions) { $permissions = $this->getAllPermissions($permissions); if($permissions === null) { return $this; } $this->permissions()->saveMany($permissions); return $this; } public function withdrawPermissionsTo( ... $permissions ) { $permissions = $this->getAllPermissions($permissions); $this->permissions()->detach($permissions); return $this; } public function refreshPermissions( ... $permissions ) { $this->permissions()->detach(); return $this->givePermissionsTo($permissions); } public function hasPermissionTo($permission) { return $this->hasPermissionThroughRole($permission) || $this->hasPermission($permission); } public function hasPermissionThroughRole($permission) { foreach ($permission->roles as $role){ if($this->roles->contains($role)) { return true; } } return false; } public function hasRole( ... $roles ) { foreach ($roles as $role) { if ($this->roles->contains('slug', $role)) { return true; } } return false; } public function roles() { return $this->belongsToMany(Role::class,'users_roles'); } public function permissions() { return $this->belongsToMany(Permission::class,'users_permissions'); } protected function hasPermission($permission) { return (bool) $this->permissions->where('slug', $permission->slug)->count(); } protected function getAllPermissions(array $permissions) { return Permission::whereIn('slug',$permissions)->get(); } } إنشاء مُزود خدمة PermissionsServiceProvider: php artisan make:provider PermissionsServiceProvider نقوم بإضافة blade directive فيه و تعريف بوابة Gate من أجل كل صلاحية في الدالة boot: <?php namespace App\Providers; use App\Models\Permission; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; class PermissionsServiceProvider extends ServiceProvider { public function register() { // } public function boot() { try { Permission::get()->map(function ($permission) { Gate::define($permission->slug, function ($user) use ($permission) { return $user->hasPermissionTo($permission); }); }); } catch (\Exception $e) { report($e); return false; } //Blade directives Blade::directive('role', function ($role) { return "if(auth()->check() && auth()->user()->hasRole({$role})) :"; //return this if statement inside php tag }); Blade::directive('endrole', function ($role) { return "endif;"; //return this endif statement inside php tag }); } } بعد ذلك نقوم بتسجيل مُزود الخدمة في مصفوفة providers في الملف config\app.php: 'providers' => [ App\Providers\PermissionsServiceProvider::class, ], الآن تحتاج إلا إضافة بعض البيانات الإختبارية ملف بذر لكل من الصلاحيات و الأدوار و تضع فيه كل صلاحيات الموقع و بعض الأدوار التي تريد أن تكون ضمن تطبيقك و تربط الصلاحيات بالأدوار ثم تربط الأدوار بالمُستخدمين. <?php $admin_permission = Permission::where('slug','create-users')->first(); $manager_permission = Permission::where('slug', 'edit-users')->first(); //RoleTableSeeder.php $admin_role = new Role(); $admin_role->slug = 'admin'; $admin_role->name = 'Admin Role'; $admin_role->save(); $admin_role->permissions()->attach($admin_permission); $manager_role = new Role(); $manager_role->slug = 'manager'; $manager_role->name = 'Assistant Manager'; $manager_role->save(); $manager_role->permissions()->attach($manager_permission); $admin_role = Role::where('slug','admin')->first(); $manager_role = Role::where('slug', 'manager')->first(); $createUsers = new Permission(); $createUsers->slug = 'create-users'; $createUsers->name = 'Create Users'; $createUsers->save(); $createUsers->roles()->attach($admin_role); $editUsers = new Permission(); $editUsers->slug = 'edit-users'; $editUsers->name = 'Edit Users'; $editUsers->save(); $editUsers->roles()->attach($manager_role); $admin_role = Role::where('slug','admin')->first(); $manager_role = Role::where('slug', 'manager')->first(); $admin_perm = Permission::where('slug','create-users')->first(); $manager_perm = Permission::where('slug','edit-users')->first(); $admin = new User(); $admin->name = 'Samir Abboud'; $admin->email = 'samir@gmail.com'; $admin->password = bcrypt('password'); $admin->save(); $admin->roles()->attach($admin_role); $admin->permissions()->attach($admin_perm); $manager = new User(); $manager->name = 'Kamel Mahmoudi'; $manager->email = 'kamel@gmail.com'; $manager->password = bcrypt('password'); $manager->save(); $manager->roles()->attach($manager_role); $manager->permissions()->attach($manager_perm); يُمكنك إستخدام tinker لعمل test: $admin = User::where('email', 'samir@gmail.com')->first(); $admin->hasRole('admin'); // لفحص إن كان المُستخدم يملك الدور مدير $admin->givePermissionsTo('edit-users') // إعطاء المُستخدم صلاحية تعديل مُستخدمين $admin->can('create-users') // فحص المُستخدم إذا كان لديه صلاحية إنشاء مُستخدم في صفحات العرض يُمكن إستخدام التوجيه الذي أنشأناه: @role('admin') مرحبا مدير @endrole الخطوة الأخيرة هي إنشاء middleware لعدم السماح للمُستخدم الوصول إلى مسار مُحدد إذا لم يكن يملك الصلاحية: php artisan make:middleware RoleMiddleware php artisan make:middleware PermissionMiddleware <?php namespace App\Http\Middleware; use Closure; class RoleMiddleware { public function handle($request, Closure $next, $role) { if (!auth()->check()) { return redirect()->route('login'); } if(!auth()->user()->hasRole($role)) { abort(403); } return $next($request); } } <?php namespace App\Http\Middleware; use Closure; class PermissionMiddleware { public function handle($request, Closure $next, $permission) { if (!auth()->check()) { return redirect()->route('login'); } if(!auth()->user()->can($permission)) { abort(403); } return $next($request); } } بعد ذلك تقوم بتسجيل هاتين الmiddleware ضمن App\Http\Kernel.php في مصفوفة routeMiddleware: <?php protected $routeMiddleware = [ . . 'role' => \App\Http\Middleware\RoleMiddleware::class, 'permission' => \App\Http\Middleware\PermissionMiddleware::class, ]; الآن يتبقى فقط الإستخدام في ملف المسارات: <?php Route::group(['middleware' => 'role:admin'], function() { // مسارات المُستخدم الذي يملك الدور مدير }); أو من خلال باني المُتحكم: public function __construct() { $this->middleware('permission:create-users')->only(['create', 'store']); } إن كنت ستستخدم حزمة جاهزة فستجد كل هذه الخصائص موجودة فيها و ستجدها قد تم إختبارها بإستعمال الإختبارات الأحادية أيضاً.
  22. يُمكن إستخدام eloquent لحذف السجلات بشكل ناعم، عند الحذف الناعم للسجل، لن تُحذف السجلات فعليًّا، وإنما ستُعيّن الخاصية deleted_at على السجل وتُحفظ في قاعدة البيانات. في حال كان السجل يملك قيمة deleted_at غير فارغة non-null، هذا يعني أنه قد حُذف. لتمكين الحذف الناعم لسجل ما نضيف ال trait SoftDeletes للمودل الذي نريد أن يُطبق الحذف الناعم: <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class User extends Model { use SoftDeletes; } و نضيف العمود deleted_at لملف التهجير الخاص بالمودل: <?php Schema::table('users', function ($table) { // other fields $table->softDeletes(); }); بهذا الشكل عند إستخدام التابع delete على أي كائن من الصنف User: <?php $user = User::find(1); $user->delete(); لن يتم حذفه فعلياً بل سيتم تعديل السجل بتعديل قيمة deleted_at إلى التاريخ الذي تم حذف السجل فيه. و سيبقى في قاعدة البيانات، و عند الإستعلام على المُستخدمين: <?php $users = User::all(); لن تظهر السجلات المحذوفة بشكل ناعم مع السجلات الكلية. هناك عدة توابع أخرى يُمكن إستخدامها لمعرفة ما إذا كان سجل ما محذوف بشكل ناعم، او جلب السجلات المحذوفة ناعماً بالإضافة إلى عملية إسترجاع هذه السجلات يُمكنك الإطلاع عليها من خلال توثيق موسوعة حسوب: الحذف الناعم (Soft Deleting)
  23. يبدو أن الأمور مُختلطة عليك قليلاً، في المُتحكم كل تابع يُؤدي مهمة مُعينة مثلاً إن قمنا بإنشاء مُتحكم PostController فالتابع index مسؤول عن إرجاع صفحة عرض واحدة و تمرير المقالات لها مثلا بهذا الشكل: <?php public function index() { $posts = Post::latest()->get(); return view("posts.index", compact('posts')); } إذا أضفنا أي كود بعد التعليمة return هذا الكود يُسمى dead code و لن يتم تنفيذه، أي ما تُحاولين القيام به غير مُمكن. بما أنك تريدين القيام بجزء لوحة التحكم و جزء الواجهة فيُنصح بفصل الجزئين عن بعضهما و هذا ما أفعله: إنشاء مُجلد خاص لمُتحكمات لوحة التحكم و ليكن Dashboard داخل مُجلد Controllers و أي مُتحكم أحتاجه في لوحة التحكم أضعه فيه. أحيانا قد يكون المشروع كبير فالأحسن إنشاء ملف مسارات خاص بلوحة التحكم أيضاً مثلاً admin.php و نضع فيه كافة مسارات لوحة التحكم سيكون بهذا الشكل مثلاً: <?php use Illuminate\Support\Facades\Route; Route::prefix('dashboard') ->name('dashboard.') ->group(function () { // هنا نضع أي مسار خاص بلوحة التحكم و أي مسار يكون مسبوق تلقائياً ب dashboard في الرابط }); الآن يجب أن نُخبر laravel أن هناك ملف مسارات جديد يجب أن تأخذه بعين الإعتبار و يتم ذلك على مُستوى مزود الخدمة RouteServiceProvider: اولاً نُضيف مجال الأسماء لمُتحكمات لوحة التحكم في مُتغير: <?php protected $dashboard_namespace = 'App\Http\Controllers\Dashboard'; حيث كافة المتحكمات الموجودة في المُجلد Dashboard الذي أنشأناه سابقاً يجب أن تكون تابعة لهذا المجال. ثانياً في التابع map نضيف سطر جديد لعمل map لمسارات ملف admin.php بهذا الشكل: <?php public function map() { $this->mapApiRoutes(); // هذا السطر يكون موجود $this->mapWebRoutes(); // هذا السطر يكون موجود ايضاً $this->mapDashboardRoutes(); // هذا ما نضيفه // } ثالثاً بعدما استدعينا التابع mapDashboardRoutes نقوم بإنشائه لأنه لن يكون موجوداً ضمن نفس مزود الخدمة: <?php protected function mapDashboardRoutes() { Route::middleware('web') ->namespace($this->dashboard_namespace) // هذا السطر لتحديد مجال الأسماء ->group(base_path('routes/admin.php')); // و هنا حددنا ملف المسارات } الآن بعدما أعددنا كل شيء نحتاج فقط لإضافة المسار الذي نُريده و المتحكم الذي نريده مثلاً نريد عرض المقالات في لوحة التحكم فنُنشئ المسار على شكل resource لأنه سيضم مُختلف العمليات في ملف مسارات لوحة التحكم admin.php: <?php use Illuminate\Support\Facades\Route; Route::prefix('dashboard') ->name('dashboard.') ->group(function () { // admins routes Route::resource('posts', 'PostController'); }); بعدما أعددنا المسار نحتاج إنشاء مُتحكم للمقالات PostController في المُجلد Dashboard الذي أنشأناه سابقاً: <?php namespace App\Http\Controllers\Dashboard; use App\Http\Controllers\Controller; use App\Post; class PostController extends Controller { public function index() { $posts = Post::latest()->get(); return view('dashboard.posts.index', compact('posts')); } } هذا يعني أننا سنُضيف مُجلد dashboard داخل مُجلد views و فيه نضع كافة صفحات العرض الخاصة بلوحة التحكم، و بما أننا نعمل على المقالات فسنضع مُجلد إسمه posts و فيه نضع كافة صفحات العرض الخاصة بالمقالات: views |_ dashboard |__|___ posts |__|____|__ index.blade.php |__|____|__ create.blade.php |__|____|__ edit.blade.php |__|____|__ show.blade.php |__|___ categories |__|____|__ index.blade.php |__|____|__ create.blade.php |__|____|__ edit.blade.php |__|____|__ show.blade.php |... هذا فيما يخص جزء لوحة التحكم، فيما يخص جزء الواجهة فسيبقى كما كان، المسارات الخاصة به سيتم وضعها في ملف web.php و المُتحكمات الخاصة به ستكون في مُجلد Controllers مُباشرة، يُمكنك التنظيم أيضاً فبما أننا وضعنا مُجلد يضم كافة صفحات العرض الخاصة بلوحة التحكم يُمكنك وضع مُجلد يضم كافة صفحات العرض الخاصة بالواجهة، الأمر يعود لك بهذا الخصوص. الآن إذا اردنا الذهاب إلى صفحة المقالات الموجودة في لوحة التحكم سنستعرض الرابط التالي: /dashboard/posts أما صفحة المقالات الخاصة بالواجهة: /posts بالتوفيق.
  24. يُمكن ذلك عن طريق: Model::latest()->get() بهذا الشكل سيتم ترتيب السجل على حسب تاريخ إنشائها أي حسب العمود created_at من الأحدث تنازلياً إلى الأقدم. إذا أردت الترتيب عن طريق عمود آخر بشكل تنازلي: Model::orderByDesc('column_name')->get(); // او Model::orderBy('column_name', "desc")->get(); أما: Model::orderBy('column_name')->get(); // او Model::orderBy('column_name', 'asc')->get(); فهما نفس الشيء و سيتم الترتيب تصاعدياً.
  25. قد تلاحظين أن في laravel هناك عدة طرق للوصول إلى ما نريد، لذلك في هذه الحالة لا يوجد فرق، whereIn تُستخدم لإنشاء الإستعلام الذي نريده و get هي التي تقوم بتطبيق الإستعلام و إرجاع النتائج، بعكس find التي تقوم بالإثنين معاً، أحياناً قد لا نريد جلب السجلات بل نريد حذفها مثلاً او إجراء عملية أخرى و في هذه الحالة لن تُساعدنا find، يُمكن إستخدامها لكن سنقوم في هذه الحالة بإجراء إستعلامات زائدة لا داعي لها. فإذا كنا نريد الحذف لماذا نجلب البيانات ما دُمنا نستطيع حذفها مُباشرة: Model::whereIn('id', [1, 2])->delete();
×
×
  • أضف...