-
المساهمات
189 -
تاريخ الانضمام
-
تاريخ آخر زيارة
آخر الزوار
6357 زيارة للملف الشخصي
إنجازات Ola Abbas
عضو نشيط (3/3)
32
السمعة بالموقع
-
نشرح في مقال اليوم بنية نموذج-قالب-عرض Model-Template-View أو MTV اختصارًا التي يعتمد عليها إطار عمل جانغو Django لتطوير الويب حيث يكون النموذج Model مسؤولًا عن التفاعل مع قاعدة البيانات، ويجب أن يقابل كل نموذج جدولًا منها، ويعبر القالب Template عن جزء الواجهة الأمامية من التطبيق، وهو الشيء الذي سيراه المستخدم، أما العرض View فهو يتضمن المنطق البرمجي لواجهة التطبيق الخلفية، ومسؤولً عن استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: الجزء الأول: البدء في إنشاء مدونة بسيطة الجزء الثاني: استخدام بنية MTV لإنشاء مدونة بسيطة الجزء الثالث: استخدام عمليات CRUD لإدارة المدونة الجزء الرابع: تطبيق المدونة الكامل الجزء الخامس: إضافة بعض الميزات المتقدمة إلى تطبيق المدونة مفهوم النماذج Models النموذج من أفضل ميزات جانغو Django، ففي أطر عمل الويب الأخرى ستحتاج إلى إنشاء نموذج وملف تهجير Migration، وملف التهجير هو مخطط Schema لقاعدة البيانات، يصف بنية قاعدة البيانات كأسماء الأعمدة وأنواعها، ويوفر النموذج واجهةً تتعامل مع معالجة البيانات بناءً على هذا المخطط، ولكنك ستحتاج إلى نموذج فقط في جانغو، ويمكن توليد ملفات التهجير المقابلة باستخدام أمر بسيط هو python manage.py makemigrations، مما يوفر عليك كثيرًا من الوقت. يحتوي كل تطبيق جانغو على ملف models.py واحد، ويجب تعريف جميع النماذج المرتبطة بالتطبيق بداخله، ويقابل كل نموذج ملف تهجير، والذي يقابل بدوره جدولًا في قاعدة البيانات. يمكنك التمييز بين الجداول الخاصة بالتطبيقات المختلفة من خلال إسناد بادئة لكل جدول تلقائيًا، حيث سيكون لجدول قاعدة البيانات المقابل لتطبيق blog الخاص بنا البادئة blog_. يوضح المثال التالي نموذجًا في الملف blog/models.py: from django.db import models class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) يمكنك بعد ذلك توليد ملف التهجير باستخدام الأمر التالي: python manage.py makemigrations ويجب أن يبدو ملف التهجير الناتج blog/migrations/0001_initial.py كما يلي: # Generated by Django 4.1.2 on 2022-10-19 23:13 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Person", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("first_name", models.CharField(max_length=30)), ("last_name", models.CharField(max_length=30)), ], ), ] يمكنك اعتبار ملف التهجير بمثابة مخطط يوضح كيف يجب أن يبدو جدول قاعدة البيانات، ويمكنك استخدام الأمر التالي لتطبيق هذا المخطط: python manage.py migrate يجب أن تبدو قاعدة بياناتك db.sqlite3 كما يلي: لا تحاول تعديل أو حذف ملفات التهجير إذا كنت مبتدئًا، ودع جانغو يطبّق كل شيء نيابةً عنك إلّا إن كنت تعرف ما تفعله تمامًا. يمكن لجانغو في معظم الحالات اكتشاف التغييرات التي أجريتها في النماذج حتى إذا حذفتَ شيئًا وتوليد ملفات الترحيل وفقًا لذلك. سينشئ النموذج في مثالنا جدول قاعدة بيانات person، وسيحتوي هذا الجدول على ثلاثة أعمدة اسمها id و first_name و last_name، حيث يُنشأ العمود id تلقائيًا كما هو موضح في ملف التهجير، ويُستخدم هذا العمود كمفتاح رئيسي Primary Key للفهرسة Indexing افتراضيًا. يسمَّىCharField() بنوع الحقل ويعرِّف نوع العمود، ويسمَّى max_length بخيار الحقل ويحدّد معلومات إضافية حول هذا العمود. أهم نواع حقول النموذج وخياراتها يوضّح الجدول التالي بعض أنواع الحقول الأكثر استخدامًا وننصحك بالاطلاع على جميع أنواع الحقول وخياراتها من توثيق جانغو الرسمي. نوع الحقل وصف عنه BigAutoField ينشئ عمودًا نوعه عدد صحيح يتزايد تلقائيًا، يُستخدم عادةً مع العمود id. BooleanField ينشئ عمودًا قيمه منطقية True أو False. DateField و DateTimeField يُستخدم لإضافة تواريخ وأوقات كما يوحي اسمه. FileField و ImageField ينشئ عمودًا لتخزين المسار الذي يؤشّر إلى الملف أو الصورة المرفوعة. IntegerField و BigIntegerField تتراوح قيم الأعداد الصحيحة Integer من -2147483648 إلى 2147483647، وتتراوح قيم الأعداد الصحيحة الكبيرة Big Integer من -9223372036854775808 إلى 9223372036854775807 SlugField الاسم المختصر Slug هو نسخة بسيطة من عنوان URL للاسم أو العنوان. CharField و TextField ينشئ كل من CharField و TextField عمودًا لتخزين السلاسل النصية Strings، ولكن يقابل TextField مربع نص أكبر في صفحة مدير جانغو Django Admin التي سنتحدث عنها لاحقًا في هذه السلسلة من المقالات. يوضح الجدول التالي بعض خيارات الحقول الأكثر استخدامًا: خيار الحقل وصف عنه blank يسمح للحقل بأن يحتوي على إدخال فارغ. choices يمنح الحقل خيارات متعددة، حيث سنوضّح ذلك لاحقًا عندما نصل إلى مدير جانغو. default يعطي الحقل قيمةً افتراضية. unique يتأكد من أن كلّ عنصر في العمود فريد، ويُستخدَم عادةً لتحديد الاسم المختصر Slug والحقول الأخرى التي يُفترَض أن تحتوي على قيم فريدة. خيارات الصنف Meta يمكنك أيضًا إضافة الصنف Class الذي هو Meta في صنف النموذج، والذي يحتوي على معلومات إضافية حول هذا النموذج مثل اسم جدول قاعدة البيانات وخيارات الترتيب والأسماء المفردة وجمعها التي يمكن أن يقرأها الإنسان كما يلي: class Category(models.Model): priority = models.IntegerField() class Meta: ordering = ["priority"] verbose_name_plural = "categories" توابع النموذج توابع النموذج هي دوال معرَّفة في صنف النموذج، تسمح بتطبيق إجراءات مخصصة على النسخة الحالية من كائن النموذج كما في المثال التالي: class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) birth_date = models.DateField() def baby_boomer_status(self): "Returns the person's baby-boomer status." import datetime if self.birth_date < datetime.date(1945, 8, 1): return "Pre-boomer" elif self.birth_date < datetime.date(1965, 1, 1): return "Baby boomer" else: return "Post-boomer" في الكود السابق، يفحص جانغو تاريخ ميلاد الشخص ويعيد حالة تمثل إن كانت فترة ولادته قبل زيادة المواليد التي حدثت بعد الحرب العالمية الثانية Pre-boomer، أم خلالها Baby-Boomer أم بعدها Post-boomer للشخص عند استدعاء التابع baby_boomer_status(). ملاحظة: الكائنات والتوابع والخاصيات مفاهيم مهمة جدًا في لغات البرمجة، لذا ننصح بمطالعة مقال كائنات وأصناف بايثون لمزيد من المعلومات. وراثة النماذج ستحتاج إلى أكثر من نموذج واحد في معظم تطبيقات الويب، وسيكون لبعضها حقول مشتركة، لذا يمكنك إنشاء نموذج أب Parent Model يحتوي على الحقول المشتركة، ثم تجعل النماذج الأخرى ترث هذا النموذج الأب كما يلي: class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() class Meta: abstract = True class Student(CommonInfo): home_group = models.CharField(max_length=5) لاحظ أن النموذج CommonInfo نموذج مجرد Abstract، مما يعني أنه لا يقابل نموذجًا فرديًا فعليًا، بل يستخدم كأب لنماذج أخرى. لنولّد الآن ملف تهجير جديد blog/migrations/0002_student.py باستخدام الأمر التالي للتحقق من ذلك: python manage.py makemigrations سيتولّد ملف التهجير التالي: # Generated by Django 4.1.2 on 2022-10-19 23:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("blog", "0001_initial"), ] operations = [ migrations.CreateModel( name="Student", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ("age", models.PositiveIntegerField()), ("home_group", models.CharField(max_length=5)), ], options={ "abstract": False, }, ), ] لاحظ إنشاء الجدول Student فقط. علاقات قاعدة البيانات تحدثنا عن كيفية إنشاء جداول فردية، ولكن لا تكون هذه الجداول مستقلة تمامًا في معظم التطبيقات، بل توجد علاقات بينها، فمثلًا قد يكون لديك فئة Category تنتمي إليها منشورات متعددة، ويكون كل منشور في المدونة تابع لمستخدم معين وما إلى ذلك، لذا توجد ثلاثة أنواع أساسية للتعبير عن العلاقات بين جداول قاعدة البيانات وهي: علاقة واحد إلى واحد One-to-one وعلاقة متعدد إلى واحد Many-to-one وعلاقة متعدد إلى متعدد Many-to-many وسنشرح تاليًا المزيد حول هذه العلاقات وطريقة وصفها ضمن نماذج جانغو. علاقة واحد إلى واحد علاقة واحد إلى واحد هي العلاقة الأسهل، فمثلًا يمكن أن يكون لكل شخص هاتف واحد، ويمكن أن يعود كل هاتف إلى شخص واحد، حيث يمكننا وصف هذه العلاقة في النماذج كما يلي: class Person(models.Model): name = models.CharField(max_length=100) class Phone(models.Model): person = models.OneToOneField('Person', on_delete=models.CASCADE) يعمل نوع الحقل OneToOneField مثل أي نوع حقل آخر، ولكنه يتطلب وسيطين على الأقل هما: الوسيط الأول هو اسم النموذج الآخر الذي يرتبط به هذا النموذج بعلاقة، والوسيط الثاني هو on_delete الذي يعرّف الإجراء الذي سيتخذه جانغو عند حذف البيانات، ويتعلق هذا المعامل بلغة SQL أكثر من جانغو، لذا لن نتحدث عنه بالتفصيل، ولكن إن كنت مهتمًا، فاطلع على بعض القيم المتاحة للمعامل on_delete. يمكنك الآن إنشاء عمليات تهجير وتطبيقها لهذه النماذج ومعرفة ما يحدث، وإذا واجهتك مشاكل أثناء تشغيل الأوامر التالية، فاحذف الملف db.sqlite3 وملفات التهجير للبدء من جديد: python manage.py makemigrations python manage.py migrate لاحظ أن نوع الحقل OneToOneField أنشأ العمود person_id في الجدول blog_phone، وسيخزّن هذا العمود معرّف id الشخص الذي يمتلك هذا الهاتف. علاقة متعدد إلى واحد يمكن أن تحتوي كل فئة على منشورات متعددة وينتمي كل منشور إلى فئة واحدة مثلًا، حيث يشار إلى هذه العلاقة باسم علاقة متعدد إلى واحد التي نعرّفها كما يلي: class Category(models.Model): name = models.CharField(max_length=100) class Post(models.Model): category = models.ForeignKey('Category', on_delete=models.CASCADE) ينشئ نوع الحقل ForeignKey العمود category_id في الجدول blog_post، والذي يخزن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تكون علاقة متعدد إلى متعدد أكثر تعقيدًا بعض الشيء، فمثلًا يمكن أن يكون لكل مقال وسوم Tags متعددة، ويمكن أن يكون لكل وسم مقالات متعددة: class Tag(models.Model): name = models.CharField(max_length=100) class Post(models.Model): tags = models.ManyToManyField('Tag') ستنشئ الشيفرة البرمجية السابقة جدولًا جديدًا بالاسم post_tags بدلًا من إنشاء عمود جديد، وسيحتوي هذا الجدول الجديد على عمودين هما post_id و tag_id، مما يتيح لك تحديد جميع الوسوم المرتبطة بمنشور معين والعكس صحيح. لنفترض أن لدينا الجدول التالي مثلًا لتوضيح الأمور: post_id tag_id 1 1 2 1 3 2 1 2 2 3 يمتلك المنشور الذي له المعرّف id=1 وسمين tags لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمور للعثور على المنشورات باستخدام الوسم، فيمكننا أن نرى منشورين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للوسم ذي المعرّف id=2. طبقة العرض View طبقة العرض مكون مهم في تطبيق جانغو ، فهي المكان الذي نكتب فيه المنطق البرمجي للواجهة الخلفية. يسترد العرض البيانات من قاعدة البيانات من خلال النموذج المقابل ويعالج هذه البيانات المستردة ويضعها في الموقع المقابل في القالب ويعرض هذا القالب ويعيده إلى المستخدم. لا تعمل دالة العرض View Function على استرداد البيانات فقط، إذ توجد أربع عمليات أساسية يمكن تطبيقها على البيانات في معظم تطبيقات الويب، وهي الإنشاء Create والقراءة Read والتحديث Update والحذف Delete، ويشار إلى هذه العمليات مجتمعة بالاسم CRUD، والتي سنوضّحها في مقال لاحق. تحدّثنا عن النماذج في القسم السابق، ولكننا ما زلنا لا نعرف كيفية استرداد البيانات أو تخزينها باستخدام النموذج، حيث يقدم جانغو واجهة برمجة تطبيقات API بسيطة لمساعدتنا في ذلك، تسمى QuerySet. لنفترض أن لدينا النموذج blog/models.py التالي: class Category(models.Model): name = models.CharField(max_length=100) class Tag(models.Model): name = models.CharField(max_length=200) class Post(models.Model): title = models.CharField(max_length=255) content = models.TextField() pub_date = models.DateField() category = models.ForeignKey(Category, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag) يمكنك معالجة البيانات بمساعدة QuerySet من خلال هذا النموذج ضمن دوال العرض التي توجد في الملف blog/views.py. إنشاء البيانات وحفظها لنفترض أنك تريد إنشاء فئة جديدة كما يلي: # استيراد نموذج الفئة Category from blog.models import Category # إنشاء نسخة جديدة من النموذج Category category = Category(name="New Category") # حفظ الفئة التي أنشأناها في قاعدة البيانات category.save() يجب أن يكون ما سبق سهلًا إذا كنت على دراية بمفهوم البرمجة كائنية التوجه، حيث أنشأنا في المثال السابق نسخة جديدة من الكائن Category واستخدمنا التابع save() الذي ينتمي إلى هذا الكائن لحفظ المعلومات في قاعدة البيانات. توجد علاقة متعدد إلى واحد بين الفئة والمنشور، وقد عرفناها باستخدام الحقل ForeignKey()، مع افتراض أن لدينا عدد كافي من السجلات في قاعدة البيانات. from blog.models import Category, Post # Post.objects.get(pk=1) هي الطريقة التي نسترجع بها المنشور الذي له المفتاح الرئيسي pk=1، # حيث يرمز pk إلى المفتاح الرئيسي Primary Key الذي يمثّل المعرّف id عادةً إن لم نحدّد خلاف ذلك. post = Post.objects.get(pk=1) # استرداد الفئة التي اسمها "New Category" new_category = Category.objects.get(name="New Category") # إسناد المتغير new_category إلى حقل فئة المنشور وحفظه post.category = new_category post.save() توجد أيضًا علاقة متعدد إلى متعدد بين المنشورات والوسوم كما يلي: from blog.models import Tag, Post post1 = Post.objects.get(pk=1) # استرداد المنشور 1 tag1 = Tag.objects.get(pk=1) # استرداد الوسم 1 tag2 = Tag.objects.get(pk=2) # استرداد الوسم 2 tag3 = Tag.objects.get(pk=3) # استرداد الوسم 3 tag4 = Tag.objects.get(pk=4) # استرداد الوسم 4 tag5 = Tag.objects.get(pk=5) # استرداد الوسم 5 post.tags.add(tag1, tag2, tag3, tag4, tag5) # إضافة الوسوم من 1 إلى 5 إلى المنشور 1 استرداد البيانات استرداد الكائنات أكثر تعقيدًا قليلًا مما رأيناه سابقًا، لذا سنوضّح فيما يلي كيف سنجد سجلًا معينًا في قاعدة بياناتنا التي تحتوي على آلاف السجلات إن لم نكن نعرف المعرّف id، وسنوضّح كيفية الحصول على مجموعة من السجلات التي تناسب معايير معينة بدلًا من الحصول على سجل واحد مثلًا. توابع QuerySet تتيح توابع QuerySet استرداد البيانات بناءً على معايير معينة، ويمكن الوصول إليها باستخدام السمة Attribute التي هي objects، حيث يُستخدَم التابع get() الذي رأيناه سابقًا لاسترداد سجل معين كما يلي: first_tag = Tag.objects.get(pk=1) new_category = Category.objects.get(name="New Category") ويمكن أيضًا استرداد جميع السجلات باستخدام التابع all(): Post.objects.all() يعيد التابع all() مجموعة من السجلات والتي نسميها مجموعة الاستعلام QuerySet، ويمكنك تحسين هذه المجموعة من خلال سَلسَلة التابع filter() أو التابع exclude() مع التابع all() كما يلي: Post.objects.all().filter(pub_date__year=2024) سيؤدي ذلك إلى إعادة جميع المنشورات المنشورة في عام 2024، ويُسمَّى pub_date__year بوسيط البحث في الحقول Field Lookup، وسنناقش هذا الموضوع بالتفصيل لاحقًا. يمكننا أيضًا استبعاد المنشورات المنشورة عام 2024 كما يلي: Post.objects.all().exclude(pub_date__year=2024) هناك أيضًا العديد من توابع QuerySet الأخرى بالإضافة إلى get() و all() و filter() و exclude()، ولكن لن نتحدث عنها جميعًا، لذا اطّلع على القائمة الكاملة لجميع توابع QuerySet من توثيق جانغو الرسمي. وسطاء عمليات البحث في الحقول Field Lookups وسطاء عمليات البحث في الحقول هي وسطاء الكلمات المفتاحية للتوابع get() و filter() و exclude()، والتي تعمل بطريقة مشابهة لتعليمة WHERE في لغة SQL، وتأخذ الصيغة fieldname__lookuptype=value مع وجود شرطة سفلية مزدوجة. Post.objects.all().filter(pub_date__lte='2024-01-01') pub_date هو اسم الحقل و lte هو نوع البحث الذي يرمز إلى أقل من أو يساوي، وستُعاد جميع المنشورات التي يكون فيها تاريخ النشر pub_date أقل من أو يساوي 2024-01-01. يمكن أيضًا استخدام وسطاء عمليات البحث في الحقول للعثور على السجلات التي لها علاقة بالسجل الحالي كما في المثال التالي، وستُعاد جميع المنشورات التي تنتمي إلى الفئة التي اسمها "Django": Post.objects.filter(category__name='Django') يمكننا تطبيق ذلك عكسيًا مثل إعادة جميع الفئات التي تحتوي على منشورٍ واحد على الأقل الذي يحتوي عنوانه على الكلمة "Django" كما يلي: Category.objects.filter(post__title__contains='Django') يمكننا أيضًا المرور عبر علاقات متعددة كما يلي: Category.objects.filter(post__author__name='Admin') يعيد الاستعلام هنا جميع الفئات التي تحتوي على منشورات ينشرها المستخدم Admin، إذ يمكن وضع سلسلة من العلاقات بالعدد الذي نريده. ويمكنك مطالعة جميع وسطاء عمليات البحث في الحقول التي يمكنك استخدامها من توثيق جانغو الرسمي. حذف الكائنات نستخدم التابع delete() لحذف سجلٍ ما، حيث ستحذف الشيفرة البرمجية التالية المنشور الذي له المفتاح الرئيسي pk=1: post = Post.objects.get(pk=1) post.delete() ويمكننا أيضًا استخدامه لحذف سجلات متعددة معًا كما يلي: Post.objects.filter(pub_date__year=2022).delete() سيؤدي ذلك إلى حذف جميع المنشورات في عام 2022، ولكن قد يتعلّق السجل الذي نحذفه بسجل آخر مثل محاولة حذف فئة تحتوي على منشورات متعددة كما يلي: category = Category.objects.get(pk=1) category.delete() يحاكي جانغو سلوك قيد SQL التالي ON DELETE CASCADE، مما يعني حذف جميع المنشورات التي تنتمي إلى هذه الفئة أيضًا، ولكن إذا أردتَ تغيير ذلك، فيمكنك تغيير خيار on_delete إلى شيء آخر، لذا اطّلع على جميع خيارات on_delete المتاحة في توثيق جانغو الرسمي. دالة العرض View Function وضّحنا ما يمكن فعله داخل دالة العرض، وسنوضّح كيف تبدو دالة العرض الكاملة كما في المثال التالي، حيث تُعرَّف جميع العروض ضمن الملف views.py: from django.shortcuts import render from blog.models import Post # أنشئ عروضك الخاصة هنا def my_view(request): posts = Post.objects.all() return render(request, 'blog/index.html', { 'posts': posts, }) هناك شيئان يجب الانتباه إليهما في هذا المثال، أولهما أن دالة العرض تأخذ المتغير request كدخل، وهذا المتغير هو كائن HttpRequest يُمرَّر تلقائيًا إلى العرض من موجّه إرسال Dispatcher عناوين URL. اطّلع على مقال مدخل إلى HTTP لمزيد من المعلومات حول التواصل بين عميل وخادم التطبيق. يحتوي request على الكثير من المعلومات حول طلب HTTP الحالي، فمثلًا يمكننا الوصول إلى تابع طلب HTTP وكتابة شيفرات برمجية مختلفة لتوابع مختلفة كما يلي: if request.method == 'GET': do_something() elif request.method == 'POST': do_something_else() ملاحظة: اطّلع على جميع المعلومات التي يمكنك الوصول إليها من كائن request في توثيق جانغو الرسمي. والشيء الآخر الذي يجب الانتباه إليه هو استيراد اختصار Shortcut اسمه render()، ثم استخدامه لتمرير المتغير posts إلى القالب blog/index.html. يُطلق عليه اختصار لأنه من المفترض تحميل القالب افتراضيًا باستخدام التابع loader() وعرض هذا القالب مع البيانات المُسترَدة وإعادة كائن HttpResponse، ولكن بسّط جانغو هذه العملية باستخدام الاختصار render(). لن نتحدث عن الطريقة المعقدة لذلك، لأننا لن نستخدمها في هذا المقال ويمكنك مطالعة جميع دوال الاختصار Shortcut Functions في توثيق جانغو الرسمي. نظام قوالب جانغو طبقة القوالب Template هي جزء الواجهة الأمامية لتطبيق جانغو، لذا تكون ملفات القالب هي شيفرات مكتوبة بلغة HTML لأنها تمثّل ما تراه في المتصفح، ولكن قد تكون الأمور أكثر تعقيدًا قليلًا، فإن احتوى القالب على شيفرات HTML فقط، فسيكون موقع الويب ساكنًا بالكامل، ولا نريد ذلك، لذا يجب أن يخبر القالبُ دالةَ العرض بمكان وضع البيانات المسترَدة. عمليات الضبط Configurations يجب أولًا تغيير شيء ما في الملف settings.py، إذ يجب أن تخبر جانغو بمكان وضع ملفات القوالب، لذا لننشئ المجلد templates، حيث اخترنا وضعه ضمن المجلد الجذر للمشروع، ولكن يمكنك نقله لمكان آخر. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles ├── staticfiles └── templates انتقل إلى الملف settings.py وابحث عن TEMPLATES. TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ 'templates', ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] لنعدّل الخيار DIRS الذي يؤشر إلى المجلد templates، ولنتحقق الآن من عمل هذا الإعداد، لذا ننشئ نمط عنوان URL جديد يؤشّر إلى العرض test() في الملف djangoBlog/urls.py كما يلي: from django.urls import path from blog import views urlpatterns = [ path('test/', views.test), ] ولننشئ الآن العرض test() في الملف blog/views.py كما يلي: def test(request): return render(request, 'test.html') انتقل إلى المجلد templates وأنشئ القالب test.html كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test Page</title> </head> <body> <p>This is a test page.</p> </body> </html> شغّل خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/، وستظهر الصفحة التالية: لغة قوالب جانغو لنناقش الآن محرّك قوالب جانغو بالتفصيل، وتذكّر أنه يمكننا إرسال البيانات من العرض إلى القالب كما يلي: def test(request): return render(request, 'test.html', { 'name': 'Jack' }) تُسنَد السلسلة النصية 'Jack' إلى المتغير name وتمرَّر إلى القالب، ويمكننا عرض المتغير name ضمن القالب باستخدام أقواس معقوصة مزدوجة {{ }} كما يلي: <p>Hello, {{ name }}</p> حدّث المتصفح، وسترى النتيجة التالية: ولكن لا تكون البيانات المُمرَّرة إلى القالب سلسلة نصية بسيطة في أغلب الحالات كما في المثال التالي: def test(request): post = Post.objects.get(pk=1) return render(request, 'test.html', { 'post': post }) المتغير post في المثال السابق هو قاموس Dictionary، حيث يمكنك الوصول إلى العناصر الموجودة في هذا القاموس في القالب كما يلي: {{ post.title }} {{ post.content }} {{ post.pub_date }} المرشحات Filters تحوّل المرشحات قيم المتغيرات، فمثلًا إذا كان لدينا المتغير django الذي قيمته 'the web framework for perfectionists with deadlines' ووضعنا المرشح title مع هذا المتغير كما يلي: {{ django|title }} فسيُحوَّل القالب إلى ما يلي: The Web Framework For Perfectionists With Deadlines اطّلع على جميع المرشحات المُضمَّنة في جانغو من توثيق جانغو الرسمي. الوسوم Tags تضيف الوسوم ميزات لغات البرمجة مثل التحكم في التدفق والحلقات إلى شيفرة HTML، مما يوفر الكثير من الوقت والموارد، إذ لن نضطر إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا. تُعرَّف جميع الوسوم باستخدام {% %} مثل حلقة for التالية: <ul> {% for athlete in athlete_list %} <li>{{ athlete.name }}</li> {% endfor %} </ul> وتكون تعليمة if كما يلي: {% if somevar == "x" %} This appears if variable somevar equals the string "x" {% endif %} وتكون تعليمة if-else كما في المثال التالي: {% if athlete_list %} Number of athletes: {{ athlete_list|length }} {% elif athlete_in_locker_room_list %} Athletes should be out of the locker room soon! {% else %} No athletes. {% endif %} اطّلع على جميع الوسوم المُضمَّنة في جانغو من توثيق جانغو الرسمي، حيث توجد الكثير من المرشحات والوسوم المفيدة الأخرى في نظام قوالب جانغو، والتي سنتحدث عنها لاحقًا. نظام الوراثة Inheritance الفائدة الأساسية لاستخدام قوالب جانغو هي أنك لست بحاجة إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا، فمثلًا يوجد شريط تنقل Navigation Bar وتذييل Footer في جميع صفحات تطبيق الويب النموذجي، ومن الصعب تكرار هذه الشيفرة البرمجية في كل صفحة من صيانة هذا التطبيق، لذا يقدم جانغو طريقة سهلة للغاية لحل هذه المشكلة كما سنوضح فيما يلي. لننشئ الملف layout.html في المجلد templates، ويمثّل هذا الملف المكان الذي نعرّف فيه تخطيط القالب الخاص بنا كما يلي، ولكننا لم نضع الشيفرة البرمجية الخاصة بالتذييل وشريط التنقل لتسهيل القراءة: <!DOCTYPE html> <html> <head> {% block meta %} {% endblock %} <-- استورد شيفرة CSS هنا –!> </head> <body> <div class="container"> <!-- ضع شيفرة شريط التنقل هنا --> {% block content %} {% endblock %} <!-- ضع شيفرة تذييل الصفحة هنا --> </div> </body> </html> لاحظ أننا عرّفنا كتلتين في هذا الملف هما meta و content باستخدام الوسم {% block ... %}، وعرّفنا القالب home.html التالي لاستخدام هذا التخطيط: {% extends 'layout.html' %} {% block meta %} <title>Page Title</title> <meta charset="UTF-8"> <meta name="description" content="Free Web tutorials"> <meta name="keywords" content="HTML, CSS, JavaScript"> <meta name="author" content="John Doe"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock %} {% block content %} <p>This is the content section.</p> {% include 'vendor/sidebar.html' %} {% endblock %} سيجد جانغو أولًا الملف layout.html عند استدعاء القالب السابق، ويملأ الكتلتين meta و content بالمعلومات الموجودة في صفحة home.html. لاحظ وجود شيء آخر في هذا القالب، حيث تخبر التعليمة {% include 'vendor/sidebar.html' %} جانغو بالبحث عن القالب templates/vendor/sidebar.html ووضعه في الصفحة، حيث يتضمن القالب sidebar.html المحتويات التالية مثلًا: <p>This is the sidebar.</p> لا يُعَد ذلك شريطًا جانبيًا، ولكن يمكننا استخدامه لتوضيح عمل نظام الوراثة، وتأكد أيضًا من صحة العرض كما يلي: from django.shortcuts import render def home(request): return render(request, 'home.html') وتأكّد من أن موجّه إرسال عنوان URL الخاص بك يؤشّر إلى هذا العرض كما يلي: path('home/', views.home), افتح متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/home، ويجب أن ترى الصفحة التالية: ترجمة -وبتصرّف- للمقال Django for Beginners #2 - The MTV Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: جانغو للمبتدئين الجزء الأول: البدء في إنشاء مدونة بسيطة إنشاء تطبيق حديث باستخدام Django و Vue | الجزء الأول: الأساسيات العروض والقوالب في Django - الجزء الأول العروض والقوالب في Django - الجزء الثاني مدخل إلى إطار عمل الويب جانغو Django
-
يمكن ضرب المصفوفة بمصفوفة عمودية إذا كانت أبعادهما تسمح بذلك، حيث يُعَد ذلك عمليةً أساسيةً في الرسوميات الحاسوبية وفي العديد من المجالات الأخرى؛ إذ تُجرَى هذه العملية ملايين المرات في الثانية عند تشغيل برنامج الرسوميات ثلاثية الأبعاد. سنوضح في هذا المقال المواضيع التالية: أبعاد معامَلات ونتيجة ضرب المصفوفات بمصفوفة عمودية. أبعاد معامَلات ونتيجة ضرب مصفوفة سطرية بالمصفوفات. ضرب المصفوفات بمصفوفة عمودية. ضرب مصفوفة سطرية بالمصفوفات. استخدام الجداء النقطي لضرب المصفوفات. الوصول إلى نتيجة ضرب المصفوفات من خلال تطبيق عمليات متعددة من الجداء النقطي. قلب Flipping المصفوفة العمودية عند ضرب المصفوفات بمصفوفة عمودية. قلب أعمدة المصفوفة عند ضرب مصفوفة سطرية بها. التعريفات الرياضية لعمليات ضرب المصفوفات وخطوات تنفيذها. الخاصية غير التبديلية لضرب المصفوفات. رأينا مسبقًا عمليات شبيهة بالضرب، والتي هي: ضرب عدد حقيقي (مقدار سلمي) Scalar بعدد حقيقي، والذي يعطي عددًا حقيقيًا. ضرب مصفوفة عمودية بعدد حقيقي، والذي يعطي مصفوفة عمودية. الجداء النقطي Dot Product للمصفوفات العمودية، والذي يعطي عددًا حقيقيًا. ضرب المصفوفات بالمصفوفة العمودية ناتج ضرب مصفوفةٍ ما بمصفوفة عمودية هو مصفوفة عمودية كما في المثال التالي: سنشرح لاحقًا تفاصيل كيفية تطبيق هذه العملية، ولكن لاحظ الآن أبعاد معامَلات العملية ونتيجتها؛ إذ يمكن تشكيل عملية ضرب مصفوفة بمصفوفة عمودية إذا كان عدد أعمدة المصفوفة مساويًا لعدد أسطر المصفوفة العمودية كما يلي: Matrix(R x N) x Col. Matrix(N x 1) = Resulting Col. Matrix(R x 1) يمكن توضيح ذلك في المثال التالي: Matrix(2 x 2) x Col. Matrix(2 x 1) = Resulting Col. Matrix(2 x 1) نعرض الأبعاد بالشكل: "Row x Column" (بحيث يكون Row هو عدد الصفوف وColumn هو عدد الأعمدة)، إذ تكون المصفوفة التي أبعادها R x 1 مصفوفةً عمودية، لأنها تحتوي على عمود واحد. لاحظ أيضًا كيف تكون أبعاد نتيجة ضرب مصفوفة بمصفوفة عمودية في المثال التالي: Matrix(5 x 5) x Col. Matrix(5 x 1) = Resulting Col. Matrix(5 x 1) لنحدّد الآن ما إذا كان من الممكن ضرب مصفوفةٍ بمصفوفة عمودية وما هي أبعاد نتيجة عملية الضرب في الأمثلة الموجودة في الجدول التالي: أبعاد المصفوفة أبعاد المصفوفة العمودية أبعاد النتيجة الأبعاد 3 x 3 الأبعاد 3 x 1 الأبعاد 3 x 1 الأبعاد 3 x 2 الأبعاد 2 x 1 الأبعاد 3 x 1 الأبعاد 2 x 3 الأبعاد 2 x 1 غير ممكنة الأبعاد 4 x 3 الأبعاد 3 x 1 الأبعاد 4 x 1 الأبعاد 3 x 5 الأبعاد 5 x 1 الأبعاد 3 x 1 لاحظ أنه من غير الممكن تشكيل عملية الضرب التي تبدو كما يلي: Matrix(2 x 1) x Col. Matrix(2 x 2) = Resulting Col. Matrix(غير ممكنة) وضع المصفوفة العمودية على الجهة اليمنى فقط من عملية الضرب إذا ضربنا مصفوفةً مستطيلةً بمصفوفة عمودية، فستكون المصفوفة العمودية دائمًا على يمين المصفوفة المستطيلة؛ إذ لا يمكن ضربهما إّلا إذا كان "البعد الداخلي Inner Dimension" (البعد M مثلًا) لكل مصفوفة متساويًا: Matrix(N x M) x Col. Matrix(M x 1) = Resulting Col. Matrix(N x 1) حيث يمكن تشكيل عملية الضرب التي تبدو كما يلي: Matrix(1 x 2) x Col. Matrix(2 x 1) = عدد حقيقي(1 x 1) استخدام الجداء النقطي لضرب المصفوفات يبدو استخدام الجداء النقطي لضرب المصفوفات ممكنًا من خلال النظر إلى الأبعاد المصفوفات، ولكنه ليس دليلًا على إمكانية ذلك. إليك المثال التالي: قد يبدو غريبًا بعض الشيء عَدُّ العدد الحقيقي (المقدار السلمي Scalar) كائنًا أبعاده "1 × 1"، ولكن تبقي هذه الطريقة الأمور متناسقةً فيما بينها. لاحظ أن هذا المثال يشبه الجداء النقطي (وهو كذلك فعلًا)، وبالتالي ستتشكّل عملية الضرب كما يلي: إذًا لنوجد الآن ناتج عملية الضرب التالية كما يلي: قلب المصفوفة العمودية من المفيد التفكير في "قلب" المصفوفة العمودية عند تشكيل الجداء النقطي ليحاذي كل صف من المصفوفة، ثم ضرب وجمع العناصر المتقابلة كما في المثال التالي: وفي المثال التالي أيضًا: لنشكّل الآن عملية الضرب التالية: تدريب عملي اُحسب الجداء النقطي في الجدول التالي. ستجد ناتج الجداء في عمود النتيجة: المعامل الأول المعامل الثاني النتيجة المصفوفة المصفوفة العمودية الناتج -2 المصفوفة المصفوفة العمودية الناتج -2 المصفوفة المصفوفة العمودية الناتج 2 سنستخدم عمليات الجداء النقطي السابقة لإيجاد نتيجة ضرب المصفوفة بالمصفوفة العمودية كما يلي: إذًا يمكننا الحصول على ناتج عملية ضرب مصفوفةٍ بمصفوفة عمودية من خلال تطبيق الجداء النقطي للمصفوفة العمودية مع كل صف من المصفوفة الأخرى، ولكن يجب أن تسمح أبعاد كل معامَل بذلك؛ إذ يساعد ذلك على قلب المصفوفة العمودية، بحيث يمكن محاذاة عناصرها مع عناصر المصفوفة كما يلي: اقلب المصفوفة العمودية واحسب النتيجة ما يلي: إليك أيضًا بعض عمليات الضرب الأخرى التي قد ترغب في التدرب عليها، ولكن تذكّر أن تقلب المصفوفة العمودية وتضعها فوق المصفوفة، ثم طبّق الجداء النقطي للمصفوفة العمودية التي قلبناها مع صف واحد من المصفوفة في كل مرة: العملية النتيجة الناتج الناتج الناتج استخدام عمليات متعددة من الجداء نقطي لإجراء عملية ضرب المصفوفة السطرية بالمصفوفات جرّب الآن إجراء العملية التالية: لاحظ أن أبعاد المصفوفات متوافقة، لذا يمكن إجراء العملية كما يلي: يمكنك أن ترى من المثال السابقة أنه يمكن إجراء عملية ضرب مصفوفةٍ سطرية بمصفوفةٍ ما من خلال تطبيق عدة عمليات جداء نقطية، ولكن يجب قلب أعمدة المصفوفة في هذه الحالة كما يلي: وضع المصفوفة السطرية على الجهة اليسرى من عملية الضرب لا يمكن إجراء النوع التالي من عمليات الضرب، لأن الأبعاد غير صحيحة: Matrix (3 x 4) x Row Matrix (1 x 3) إذًا يجب أن تكون المصفوفة السطرية على الجهة اليسرى عند ضرب مصفوفة سطرية بمصفوفة مستطيلة، حيث تكون أبعاد المعامَلات والنتيجة كما يلي: Row Matrix (1 x M) x Matrix (M x C) = Result Row Matrix (M x C) ملاحظة: المصفوفة التي أبعادها 1xC هي مصفوفة سطرية مكوَّنة من عددٍ "C" من الأعمدة. نوضّح في الأمثلة الموجودة في الجدول التالي ما إذا كانت عملية الضرب ممكنة وما هي أبعاد نتيجة العملية: المعامل الأول المعامل الثاني النتيجة 3 x 3 3 x 1 3 x 1 1 x 2 2 x 2 1 x 2 1 x 2 2 x 3 1 x 3 4 x 4 3 x 1 غير ممكنة 1 x 5 5 x 3 1 x 3 لنحاول الآن إجراء عملية الضرب التالية: وينتج ما يلي: التعريفات الدقيقة لعمليات الضرب قد تشعر بعدم الارتياح عند تعريف العمليات الرياضية بهذه الطريقة غير الرسمية، لذا إليك بعض التعريفات الأدق: ضرب مصفوفة بمصفوفة عمودية Matrix times Column Matrix: ليكن لدينا المصفوفة A التي أبعادها M x C، والمصفوفة العمودية x التي أبعادها C x 1، ويكون ناتج الجداء Ax مصفوفة عمودية أبعادها M x 1؛ إذ يتشكّل عنصرها i من ناتج الجداء النقطي للصف i من المصفوفة A مع المصفوفة العمودية x. ضرب مصفوفة سطرية بمصفوفة: ليكن لدينا المصفوفة السطرية x التي أبعادها 1 x R والمصفوفة **A التي أبعادها R x M، ويكون ناتج الجداء *xA* مصفوفة سطرية أبعادها 1 x M، حيث يتشكّل عنصرها الأول من ناتج الجداء النقطي للمصفوفة السطرية x مع العمود i من المصفوفة A. لنحاول الآن إجراء عملية الضرب التالية: وسينتج لدينا ما يلي: خطوات إجراء عملية ضرب المصفوفات يتضمن الضرب دائمًا عمليات جداء نقطية للصفوف الموجودة على اليسار والأعمدة الموجودة على اليمين وفقًا للتعريفات السابقة، لذا يجب اتباع الخطوات التالية لإجراء أيٍّ من هذه الحسابات: دراسة أبعاد معاملات عملية الضرب حيث: يجب أن تكون الأبعاد الداخلية متساوية: 1xN و NxC. نتجاهل الأبعاد الداخلية للعثور على أبعاد النتيجة: 1xC. نكتب مصفوفة فارغة بالأبعاد الصحيحة لتمثّل المصفوفة الناتجة. ملء جميع عناصر المصفوفة الناتجة كما يلي: نقلب كل عمود موجود على اليمين إلى صف على اليسار. ينتج كل عنصر من عناصر المصفوفة الناتجة عن عملية جداء نقطي. تدريب عملي لا تُعَد Ax و xA متساويتين؛ فإذا كان تطبيق عملية الضرب Ax ممكنة، فستكون أبعاد xA غير متوافقة عند ذلك، ويمكن في بعض الأحيان تشكيل كلٍّ من هاتين العمليتين، ولكن النتائج ستكون مختلفة، وبالتالي يمكن القول أن ضرب المصفوفات ليست عمليةً تبديلية. إذًا لنختبر مهاراتك في الأمثلة التالية: العملية النتيجة الناتج الناتج الناتج وصلنا إلى نهاية هذا المقال الذي تعرّفنا من خلاله على ضرب مصفوفة بمصفوفة عمودية وضرب مصفوفة سطرية بمصفوفةٍ ما، وسنتعرّف في المقال التالي على كيفية ضرب مصفوفة مستطيلة بمصفوفة أخرى. ترجمة -وبتصرُّف- للفصل Matrix-Column Matrix Multiplicaton من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D الجداء الشعاعي في التصاميم ثلاثية الأبعاد وخاصياته وحسابه كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد
-
جانغو Django هو إطار عمل ويب عالي المستوى مجاني ومفتوح المصدر ومكتوب بلغة البرمجة بايثون Python، ويُستخدَم على نطاق واسع لبناء تطبيقات ويب معقدة، وهو معروف بقدرته على التعامل مع حركة المرور العالية وميزات الأمان الخاصة به ومكوناته القابلة لإعادة الاستخدام. يتبع جانغو نمط معمارية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا) ويأتي مع واجهة مُدمَجة لإدارة الموقع مع دعم الاستيثاق Authentication وربط الكائنات بالعلاقات Object-Relational Mapping (أو ORM اختصارًا) الذي يسمح بتعريف مخطط قاعدة بياناتك باستخدام أصناف Classes بايثون. يتمتع جانغو بمجتمع كبير ونشط يوفر مجموعة كبيرة من البرامج التعليمية والحزم والموارد الأخرى التي يمكن للمطورين استخدامها، إذ تُبنَى العديد من المواقع الإلكترونية المعروفة وذات حركة المرور العالية -مثل إنستغرام Instagram وبنترست Pinterest وموقع واشنطن تايمز The Washington Times- باستخدام إطار عمل جانغو. يُعَد جانغو أحد أكثر أطر عمل الويب شعبيةً وقوة للغة بايثون، وتستخدمه كلٌّ من الشركات الناشئة والكبيرة على نطاق واسع، مما يجعله خيارًا رائعًا لبناء تطبيقات ويب قوية وقابلة للتوسّع، وتؤدي مرونته وقابليته للتوسع إلى جعله الخيار الأفضل للمطورين والمؤسسات التي تريد إنشاء تطبيقات ويب معقدة وصغيرة الحجم. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: الجزء الأول: البدء في إنشاء مدونة بسيطة الجزء الثاني: استخدام بنية MTV لإنشاء مدونة بسيطة الجزء الثالث: استخدام عمليات CRUD لإدارة المدونة الجزء الرابع: تطبيق المدونة الكامل الجزء الخامس: إضافة بعض الميزات المتقدمة إلى تطبيق المدونة تثبيت الأدوات اللازمة توجد بعض الأدوات التي يجب تثبيتها على جهازك قبل البدء، حيث تحتاج مبدئيًا لغة برمجة (بايثون)، وقاعدة بيانات (SQLite)، وخادم (سنستخدم خادم التطوير المُدمَج مع جانغو) لتشغيل التطبيق بنجاح. تذكّر أن هذه الأدوات مُخصَّصة لبيئة التطوير Dev Environment المخصصة لإنشاء واختبار التطبيق فقط، لذا لا يجب استخدام هذه الأدوات في بيئة الإنتاج Production Environment. تحتاج أيضًا إلى بيئة تطوير متكاملة IDE مثل بيئة باي تشارم PyCharm أو محرّر شيفرات برمجية على الأقل، حيث سنستخدم في هذا المقال المحرّر VS Code لأنه برنامج مجاني. اتبّع الآن الخطوات التالية للحصول على هذه الأدوات: تنزيل بايثون. تنزيل SQLite. تنزيل VS Code. تنزيل PyCharm. إنشاء مشروع جانغو جديد حان الوقت لبدء كتابة الشيفرة البرمجية بعد تثبيت الأدوات السابقة، لذا أنشئ أولًا مجلد عمل جديد باستخدام الأمر التالي لتنظيم كل شيء: mkdir django-demo ثم انتقل إلى هذا المجلد باستخدام الأمر التالي: cd django-demo أنشئ بعد ذلك بيئة افتراضية جديدة للغة بايثون ضمن مجلد العمل باستخدام الأمر التالي، حيث ستكون هذه البيئة الافتراضية معزولة عن بيئة بايثون في نظامك ومشاريع بايثون الأخرى التي قد تكون على جهازك. python -m venv env إذا استخدمتَ نظام لينكس Linux أو ماك macOS، فقد تضطر إلى تشغيل أمر python3 بدلًا من python، ولكننا سنستخدم python للتبسيط. سيعمل الأمر السابق على إنشاء المجلد env الذي يحتوي على البيئة الافتراضية التي أنشأتها، ويمكنك تنشيط هذه البيئة الافتراضية باستخدام الأمر التالي: source env/bin/activate وإذا استخدمتَ نظام ويندوز Windows، فاستخدم الأمر التالي بدلًا من ذلك: env/Scripts/activate إذا نشطت البيئة الافتراضية بنجاح، فسيكون موجّه سطر الأوامر الخاص بك كما يلي: (env) eric@djangoDemo:~/django-demo$ ملاحظة: يعني (env) هنا أنك تعمل حاليًا في بيئة افتراضية اسمها env. حان الوقت الآن لتهيئة مشروع جانغو جديد، حيث تحتاج إلى تثبيت حزمة Django من خلال تشغيل الأمر التالي: python -m pip install Django ثم يمكنك استخدام أمر django-admin لإنشاء مشروع جانغو جديد كما يلي: django-admin startproject djangoBlog وسينشَأ المجلد الجديد djangoBlog التالي: يُفضَّل إعادة هيكلة المشروع بعض الشيء بحيث نبدأ من المجلد الجذر للمشروع كما يلي، ولكن لا حاجة لذلك إن لم ترغب في ذلك. إنشاء تطبيق المدونة لا يزال المشروع فارغًا حاليًا، ولاحظ أن بنية هذا المشروع بسيطة مقارنةً ببنية مشروع لارافيل Laravel، وسنناقش كل ملف في مجلد المشروع بالتفصيل لاحقًا. يتيح جانغو إنشاء تطبيقات متعددة في مشروع واحد، فمثلًا يمكن أن يكون هناك تطبيق blog وتطبيق gallery وتطبيق forum ضمن مشروع واحد. يمكن أن تتشارك هذه التطبيقات في نفس الملفات الثابتة (ملفات CSS وجافاسكربت JavaScript) والصور ومقاطع الفيديو أو يمكن أن تكون مستقلة تمامًا عن بعضها البعض، إذ يعتمد ذلك على احتياجاتك الخاصة. سننشئ في هذا المقال تطبيق مدونة blog فقط، لذا نفّذ الأمر التالي في الطرفية: python manage.py startapp blog يجب أن ترى مجلد blog جديد ضمن مجلد جذر المشروع، ويمكنك سرد محتوى المجلد باستخدام الأمر التالي: ls وإذا أردتَ تضمين الملفات المخفية أيضًا في النتيجة، فاستخدم الأمر التالي: ls -a وإذا أردتَ رؤية بنية الملفات فاستخدم الأمر التالي: tree ولكن قد تحتاج إلى تثبيت برنامج tree ليعمل الأمر السابق بنجاح اعتمادًا على نظام تشغيلك. سنستخدم الأمر السابق لعرض بنية الملفات، حيث يجب أن يكون لمشروعك البنية التالية حاليًا: . ├── blog │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoBlog │ ├── asgi.py │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── bin │ ├── include │ ├── lib │ ├── lib64 -> lib │ └── pyvenv.cfg └── manage.py يجب بعد ذلك تسجيل تطبيق blog الجديد في جانغو، لذا انتقل إلى الملف settings.py وابحث عن القائمة INSTALLED_APPS: INSTALLED_APPS = [ 'blog', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] بدء تشغيل الخادم يمكنك الآن بدء تشغيل خادم التطوير لاختبار نجاح عمل كل شيء، لذا افتح الطرفية وشغّل الأمر التالي: python manage.py runserver وسيظهر الخرج التالي في واجهة سطر الأوامر: Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. October 19, 2022 - 01:39:33 Django version 4.1.2, using settings 'djangoBlog.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. افتح المتصفح وانتقل إلى العنوان http://127.0.0.1:8000/، وستظهر الصفحة التالية: بنية التطبيق لنتحدث عن البنية التالية لتطبيق جانغو الجديد وما يفعله كل ملف قبل أن نبدأ في كتابة الشيفرة البرمجية: . ├── blog │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoBlog │ ├── asgi.py │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── env │ ├── bin │ ├── include │ ├── lib │ ├── lib64 -> lib │ └── pyvenv.cfg └── manage.py المجلد الجذر يحتوي المجلد الجذر على الملفات والمجلدات التالية: manage.py: يمثل أداة سطر الأوامر التي تتيح لك التفاعل مع مشروع جانغو بطرق مختلفة. djangoBlog: مجلد المشروع الرئيسي الذي يحتوي على ملف الإعدادات ونقطة الدخول إلى المشروع. blog: يمثل تطبيق المدونة blog. مجلد المشروع يحتوي هذا المجلد على الملفات التالية: djangoBlog/__init__.py: ملف فارغ يخبر بايثون بأن هذا المجلد يجب عَدّه حزمة بايثون. djangoBlog/settings.py: ملف الإعدادات أو الضبط Configuration لمشروع جانغو. djangoBlog/urls.py: يحتوي على التصريحات عن عناوين URL لمشروع جانغو، ويمثل جدول محتويات تطبيق جانغو الخاص بك، حيث سنتحدث عنه أكثر لاحقًا. djangoBlog/asgi.py: يُعَد نقطة الدخول لخوادم الويب المتوافقة مع واجهة ASGI لتخديم مشروعك. djangoBlog/wsgi.py: نقطة الدخول لخوادم الويب المتوافقة مع واجهة WSGI لتخديم مشروعك. مجلد التطبيق يحتوي هذا المجلد على المجلدات والملفات التالية: blog/migrations: يحتوي هذا المجلد على جميع ملفات التهجير Migration لتطبيق المدونة، حيث ينشئ جانغو هذه الملفات تلقائيًا بناءً على نماذجك Models على عكس إطار عمل لارافيل. blog/admin.py: يأتي جانغو أيضًا مع لوحة مُدمَجة لإدارة الموقع، حيث يحتوي هذا الملف على جميع عمليات الضبط الخاصة بهذه اللوحة. blog/models.py: تصف النماذج Models بنية وعلاقة قاعدة البيانات، وتُنشَأ ملفات التهجير بناءً على هذا الملف. blog/views.py: يكافئ المتحكمات Controllers في لارافيل، ويحتوي على المنطق البرمجي الأساسي لهذا التطبيق. ضبط Configuring مشروع جانغو توجد بعض التغييرات التي يجب إجراؤها على الملف settings.py قبل البدء بالشروع كما سنوضح فيما يلي. المضيفون المسموح بهم تُعد القائمة ALLOWED_HOSTS قائمة بالنطاقات Domains التي يُسمَح لموقع جانغو بتخديمها، وهي إجراء أمني لمنع هجمات ترويسات مضيف HTTP أو HTTP Host Header Attacks، والتي يمكن حدوثها حتى عند إجراء العديد من عمليات ضبط خادم الويب الآمنة ظاهريًا. لاحظ أنه ما زال بإمكاننا الوصول إلى موقعنا باستخدام المضيف 127.0.0.1 حتى وإن كانت القائمة ALLOWED_HOSTS فارغة حاليًا، والسبب هو التحقق من صحة المضيف مقابل ['.localhost', '127.0.0.1', '[::1]'] عندما تكون قيمة DEBUG هي True وتكون القائمة ALLOWED_HOSTS فارغة. قاعدة البيانات DATABASES هو قاموس يحتوي على إعدادات قاعدة البيانات التي يحتاج موقعنا إلى استخدامها، حيث يستخدم جانغو قاعدة بيانات SQLite افتراضيًا، وهي قاعدة بيانات خفيفة الوزن وتتكون من ملف واحد فقط. يجب أن تكون قاعدة بيانات SQLite كافيةً لمشروعنا التجريبي الصغير، لكنها لن تناسب المواقع الكبيرة، لذا إذا أردتَ استخدام قواعد بيانات أخرى، فإليك المثال التالي: DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "<database_name>", "USER": "<database_user_name>", "PASSWORD": "<database_user_password>", "HOST": "127.0.0.1", "PORT": "5432", } } ملاحظة: يوصي جانغو باستخدام قاعدة بيانات PostgreSQL في بيئة الإنتاج. توجد العديد من البرامج التعليمية التي تعلمك كيفية استخدام جانغو مع قاعدة بيانات MongoDB، ولكنها تُعَد فكرة سيئة، إذ قد تكون قاعدة بيانات MongoDB حلًا رائعًا، ولكنها لا تعمل بطريقة جيدة مع إطار عمل جانغو. الملفات الساكنة Static Files وملفات الوسائط Media Files يجب أن نهتم أيضًا بالملفات الساكنة وملفات الوسائط، فالملفات الساكنة هي ملفات CSS وملفات جافاسكربت، وملفات الوسائط هي الصور ومقاطع الفيديو والأمور الأخرى التي قد يرفعها المستخدم. الملفات الساكنة Static Files يجب أولًا تحديد مكان تخزين هذه الملفات، حيث سننشئ المجلد static ضمن تطبيق المدونة لموقع جانغو الخاص بنا، ويُعَد هذا المجلد هو المكان الذي نخزّن فيه الملفات الساكنة لتطبيق blog أثناء التطوير. blog ├── admin.py ├── apps.py ├── __init__.py ├── migrations ├── models.py ├── static ├── tests.py └── views.py تختلف الأمور بعض الشيء في بيئة الإنتاج، إذ يجب أن ننشئ مجلدًا مختلفًا ضمن المجلد الجذر للمشروع، ولنسمّيه بالاسم staticfiles. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py └── staticfiles ثم يجب تحديد هذا المجلد في الملف settings.py كما يلي: STATIC_ROOT = "staticfiles/" نخبر بعد ذلك جانغو بعنوان URL الذي يجب استخدامه عند الوصول إلى هذه الملفات في المتصفح. ليس من الضروري أن يكون هذا العنوان هو /static، ولكن تأكد من أنه لا يتداخل مع عمليات ضبط عناوين URL الخاصة بنا والتي سنتحدث عنها لاحقًا. STATIC_URL = "static/" ملفات الوسائط Media Files نضبط ملفات الوسائط باستخدام الطريقة نفسها، حيث يمكنك إنشاء المجلد mediafiles في المجلد الجذر للمشروع كما يلي: . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles └── staticfiles ثم نحدّد الموقع وعنوان URL في الملف settings.py كما يلي: # ملفات الوسائط MEDIA_ROOT = "mediafiles/" MEDIA_URL = "media/" موجه إرسال Dispatcher عناوين URL في جانغو يحتوي مجال تطوير الويب على بنية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا)، حيث يكون النموذج Model في هذه البنية مسؤولًا عن التفاعل مع قاعدة بياناتنا، ويجب أن يقابل كلُّ نموذج جدولَ قاعدة بيانات واحد. العرض View هو جزء الواجهة الأمامية Frontend من التطبيق، وهو ما يمكن للمستخدمين رؤيته، والمتحكم Controller هو المنطق البرمجي للواجهة الخلفية للتطبيق مثل استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. جانغو هو إطار عمل ويب مُصمَّم بناءً على بنية MVC مع مصطلحات مختلفة، فبنية جانغو هي بنية نموذج-قالب-عرض Model-Template-View (أو MTV اختصارًا)، فالقالب Template هو الواجهة الأمامية، والعرض هو المنطق البرمجي للواجهة الخلفية. سنركّز في هذا المقال على فهم هذه الطبقات، ولكن يجب أولًا البدء بنقطة الدخول لكل تطبيق ويب، والتي هي موجّه إرسال Dispatcher عناوين URL، حيث يقرأ هذا الموجّه عنوان URL ويوجّه المستخدم إلى الصفحة الصحيحة عندما يكتب المستخدم عنوان URL ويضغط على مفتاح Enter. عمليات ضبط عناوين URL الأساسية يُخزَّن ضبط عناوين URL في الملف example/urls.py كما يلي: djangoBlog ├── asgi.py ├── __init__.py ├── __pycache__ ├── settings.py ├── urls.py └── wsgi.py يوضّح المثال التالي ما يجب أن يبدو عليه موجّه إرسال عناوين URL: from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), ] يقرأ موجّه الإرسال المعلومات من عنوان URL ويعيد عرضًا، ولكن لا تخلط بين هذا العرض وعرض إطار عمل لارافيل، فالعرض في جانغو يمثّل متحكّمًا في لارافيل. إذا تبع النطاقُ admin/ في هذا المثال، فسيوجّه موجّه الإرسال المستخدم إلى صفحة المدير Admin. تمرير المعاملات باستخدام موجه إرسال عناوين URL في جانغو يكون من الضروري في بعض الأحيان أخذ مقاطع من عنوان URL وتمريرها إلى العرض بوصفها معاملات إضافية، فمثلًا إذا أردتَ عرض صفحة التدوينة، فستحتاج إلى تمرير المعاملَين id أو slug الخاصين بالتدوينة إلى العرض حتى تتمكّن من استخدام هذه المعلومات الإضافية للعثور على التدوينة التي تبحث عنها. إليك فيما يلي الطريقة التي يمكننا تمرير المعاملات باستخدامها: from django.urls import path from blog import views urlpatterns = [ path('post/<int:id>', views.post), ] ستلتقط الأقواس الزاويّة جزءًا من عنوان URL كمعامل، حيث يُسمَّى int على الجانب الأيسر بمحوِّل المسار Path Converter الذي يلتقط معاملًا من النوع الصحيح، ويمكن مطابقة أيّ سلسلة نصية باستثناء المحرف / عند عدم تضمين المحوّل. لاحظ وجود اسم المعامل على الجانب الأيمن، وهو ما سنحتاج إلى استخدامه في العرض. تتوفّر محوّلات المسارات التالية افتراضيًا: str: يطابق أيّ سلسلة نصية غير فارغة باستثناء فاصل المسار '/' افتراضيًا عند عدم تضمين محوّلٍ في التعبير. int: يطابق الصفر أو أيّ عدد صحيح موجب، ويعيد قيمة من النوع int. slug: يطابق أيّ سلسلة نصية للاسم المختصر Slug، والذي يتكون من حروف أو أرقام ASCII، بالإضافة إلى محارف الشرطة والشرطة السفلية مثل building-your-1st-django-site. uuid: يطابق معرّف UUID المنسَّق، حيث يجب تضمين شرطات، ويجب أن تكون الحروف صغيرة لمنع ربط عناوين URL متعددة مع الصفحة نفسها مثل المعرّف 075194d3-6885-417e-a8a8-6c931e272f00، ويعيد نسخةً من UUID. path: يطابق أي سلسلة نصية غير فارغة مع فاصل المسار '/'، مما يتيح لك المطابقة مع مسار URL كامل بدلًا من المطابقة مع مقطعٍ من مسار URL كما هو الحال مع المحوِّل str. استخدام التعابير النمطية Regular Expressions لمطابقة أنماط عناوين URL قد يكون النمط Pattern الذي تريد مطابقته أكثر تعقيدًا في بعض الحالات، وبالتالي يمكنك استخدام التعابير النمطية Regular Expressions لمطابقة أنماط عناوين URL. يجب استخدام التابع re_path() بدلًا من التابع path() لاستخدام التعابير النمطية: from django.urls import path, re_path from . import views urlpatterns = [ path('articles/2003/', views.special_case_2003), re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive), re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive), re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail), ] استيراد عمليات ضبط عناوين URL الأخرى لنفترض أن لديك مشروع جانغو يحتوي على تطبيقات متعددة مختلفة، حيث إذا وضعت جميع عمليات ضبط عناوين URL في ملف واحد، فسيكون الأمر فوضويًا جدًا، ولكن يمكنك فصل عمليات ضبط عناوين URL ونقلها إلى تطبيقات مختلفة، فمثلًا يمكننا في مشروعنا إنشاء ملف urls.py جديد ضمن تطبيق blog كما يلي: from django.urls import path, include urlpatterns = [ path('blog/', include('blog.urls')) ] وهذا يعني أنه إذا احتوى عنوان URL على النمط http://www.mydomain.com/blog/xxxx، فسينتقل جانغو إلى الملف blog/urls.py ويطابق باقي عنوان URL. لنعرّف الآن أنماط عناوين URL لتطبيق blog باستخدام الطريقة نفسها كما يلي: from django.urls import path from blog import views urlpatterns = [ path('post/<int:id>', views.post), ] سيتطابق هذا النمط مع نمط عنوان URL الذي هو http://www.mydomain.com/blog/post/123. تسمية أنماط عناوين URL يمكنك تسمية نمط عنوان URL من خلال إعطائه معاملًا ثالثًا كما يلي: urlpatterns = [ path('articles/<int:year>/', views.year_archive, name='news-year-archive'), ] يؤدي ذلك إلى القدرة على عكس عناوين URL المطلقة من القالب، حيث سنتحدث عن ذلك لاحقًا عندما نصل إلى طبقة القوالب. تعرّفنا على أساسيات موجّه إرسال عناوين URL، ولكن لا زال هناك شيء لم نتطرق لشرحه وهو كيفية التمييز بين التوابع المختلفة لطلبات HTTP، حيث لا يمكنك في جانغو مطابقة توابع HTTP المختلفة ضمن موجه إرسال عناوين URL، إذ تتطابق جميع التوابع مع نمط عنوان URL نفسه، وبالتالي يجب استخدام التحكم في التدفق لكتابة شيفرات برمجية مختلفة لهذه التوابع في دوال العرض، حيث سنناقش ذلك بالتفصيل في المقالات اللاحقة من هذه السلسلة. ترجمة -وبتصرّف- للمقال Django for Beginners #1 - Getting Started لصاحبه Eric Hu. اقرأ أيضًا حزم بايثون الثمانية التي تسهل تعاملك مع Django مدخل إلى إطار عمل الويب جانغو Django تعرف على أمان تطبيقات جانغو بناء تطبيق مهام باستخدام جانغو Django وريآكت React
-
سننشئ في هذا المقال الأخير من هذه السلسلة جزء الواجهة الأمامية من التطبيق، ولكن لنضع خطة أولًا، حيث سيكون لدينا الصفحة الرئيسية لتطبيق المدونة التي تعرض قائمة بجميع المنشورات الحديثة، وصفحة فئة Category المنشور التي تعرض قائمة المنشورات ضمن فئة معينة، وصفحة الوسم Tag التي تعرض قائمة المنشورات التي لها وسم مُحدَّد، وصفحة المنشور التي تعرض محتوى منشور معين، وصفحة بحث تعرض قائمة المنشورات بناءً على استعلام بحث معين. كما ستحتوي جميع هذه الصفحات على شريط جانبي مع مربع بحث وقائمة الفئات والوسوم، وستحتوي صفحة المنشور أيضًا على قسم المنشورات ذات الصلة في نهايتها. تحدّثنا عن قاعدة البيانات والنماذج Models في المقال السابق، وسنبدأ الآن بالوِجهات Routes. إنشاء الوِجهات ننشئ الوِجهات في لارافيل ضمن الملف routes/web.php كما يلي: . . . // الصفحة الرئيسية Route::get('/', [PostController::class, 'home'])->name('home'); // قائمة المنشورات ضمن هذه الفئة Route::get('/category/{category}', [CategoryController::class, 'category'])->name('category'); // قائمة المنشورات التي لها هذا الوسم Route::get('/tag/{tag}', [TagController::class, 'tag'])->name('tag'); // عرض منشور واحد Route::get('/post/{post}', [PostController::class, 'post'])->name('post'); // قائمة المنشورات بناء على استعلام البحث Route::post('/search', [PostController::class, 'search'])->name('search'); . . . الصفحة الرئيسية يكون لكل من هذه الوِجهات تابع تحكم مقابل، حيث سنبدأ أولًا بالمتحكم home() إذًا لنضع ما يلي في الملف app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { /** * عرض الصفحة الرئيسية */ public function home(): View { $posts = Post::where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('home', [ 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } هناك شيئان يجب عليك ملاحظتهما في السطر 20 من الشيفرة البرمجية السابقة وهما: أولًا، تتأكد التعليمة where('is_published', true) من استرداد المقالات المنشورة فقط. ثانيًا، يُعَد التابع paginate() أحد توابع لارافيل Laravel المُضمَّنة، مما يسمح لك بإنشاء ترقيم للصفحات Pagination في تطبيقك بسهولة. يأخذ التابع paginate() عددًا صحيحًا كدخل، فمثلًا يمثّل paginate(10) عرض 10 عناصر في كل صفحة، وسيُستخدَم متغير الدخل في العديد من الصفحات، لذا يمكنك إنشاء متغير بيئة في ملف .env كما يلي، ثم يمكنك استرداده في أيّ مكان باستخدام التابع env(): . . . PAGINATE_NUM=12 لننشئ الآن عرض View الصفحة الرئيسية المقابل. اطّلع على بنية القوالب التي أنشأئاها، وإليك بنية العروض التالية: resources/views ├── category.blade.php ├── home.blade.php ├── layout.blade.php ├── post.blade.php ├── search.blade.php ├── tag.blade.php ├── vendor │ ├── list.blade.php │ └── sidebar.blade.php └── welcome.blade.php سنبدأ أولًا بالعرض layout.blade.php الذي يمثل تخطيط الصفحة كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> @vite(['resources/css/app.css', 'resources/js/app.js']) @yield('title') </head> <body class="container mx-auto font-serif"> <nav class="flex flex-row justify-between h-16 items-center border-b-2"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > <a href="{{ route('dashboard') }}" class="hover:underline hover:underline-offset-1" >Dashboard</a > <a href="#" class="hover:underline hover:underline-offset-1">Link</a> </div> </nav> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </body> </html> يستخدم لارافيل الأداة Vite افتراضيًا لتجميع أصول المشروع كما في السطر 8، وثبّتنا الحزمة Breeze من لارافيل في المقال السابق، وتستخدم هذه الحزمة إطار عمل Tailwind CSS، حيث سيستورد السطر 8 من الشيفرة البرمجية السابقة تلقائيًا الملفين app.css و app.js المقابلَين. يمكنك استخدام إطار عمل مختلف أيضًا، ولكن يجب أن تعود إلى توثيقه للحصول على تفاصيل حول كيفية استخدامه مع لارافيل أو Vite. لننشئ الآن عرض الصفحة الرئيسية resources/views/home.blade.php كما يلي: @extends('layout') @section('title') <title>Home</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection ويكون عرض قائمة المنشورات resources/views/vendor/list.blade.php كما يلي: <!-- قائمة المنشورات --> <div class="grid grid-cols-3 gap-4"> @foreach ($posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}" >Read more →</a > </div> </div> @endforeach </div> {{ $posts->links() }} يولّد السطر 9 Storage::url($post->cover) عنوان URL الذي يؤشّر إلى صورة الغلاف. لا تُعَد الطريقة التي يخزن بها لارافيل العلامات الزمنية Timestamps سهلة الاستخدام، لذلك استخدمنا حزمة Carbon في السطر 14 لإعادة تنسيق العلامات الزمنية. استخدمنا أيضًا الدالة strip_tags() في السطر 18 لإزالة وسوم HTML، ثم تحدِّد الدالة limit() الحد الأقصى لطول السلسلة، وستوضَع نقاط ... مكان الجزء الزائد منها. استخدمنا التابع paginate() لإنشاء مرقّم الصفحات في المتحكم، ولكن يمكننا عرض مرقّم الصفحات في العرض باستخدام التعليمة {{ $posts->links() }} الموجودة في السطر 30. لننشئ الآن عرض الشريط الجانبي resources/views/vendor/sidebar.blade.php كما يلي: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="{{ route('search') }}" method="POST" class="grid grid-cols-4 gap-2" > {{ csrf_field() }} <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1" > Search </button> </form> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Categories</div> <div class="p-4"> <ul class="list-none list-inside"> @foreach ($categories as $category) <li> <a href="{{ route('category', ['category' => $category->id]) }}" class="text-blue-500 hover:underline" >{{ $category->name }}</a > </li> @endforeach </ul> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Tags</div> <div class="p-4"> @foreach ($tags as $tag) <span class="mr-2" ><a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" >{{ $tag->name }}</a ></span > @endforeach </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">More Card</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">...</div> <div class="p-4"> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat, voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic reprehenderit pariatur autem totam, voluptates non officia accusantium rerum unde provident! </p> </div> </div> </div> صفحة الفئة Category ضع ما يلي في ملف المتحكم app/Http/Controllers/CategoryController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الفئة */ public function category(string $id): View { $category = Category::find($id); $posts = $category->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('category', [ 'category' => $category, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } ويكون عرض الفئة المقابل resources/views/category.blade.php كما يلي: @extends('layout') @section('title') <title>Category - {{ $category->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection صفحة الوسم Tag ضع ما يلي في ملف المتحكم app/Http/Controllers/TagController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة المنشورات التي تنتمي إلى الوسم */ public function tag(string $id): View { $tag = Tag::find($id); $posts = $tag->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('tag', [ 'tag' => $tag, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags ]); } . . . } ويكون عرض الوسم المقابل resources/views/tag.blade.php كما يلي: @extends('layout') @section('title') <title>Tag - {{ $tag->name }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection صفحة المنشور ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض المنشور المطلوب */ public function post(string $id): View { $post = Post::find($id); $categories = Category::all(); $tags = Tag::all(); $related_posts = Post::where('is_published', true)->whereHas('tags', function (Builder $query) use ($post) { return $query->whereIn('name', $post->tags->pluck('name')); })->where('id', '!=', $post->id)->take(3)->get(); return view('post', [ 'post' => $post, 'categories' => $categories, 'tags' => $tags, 'related_posts' => $related_posts ]); } . . . } لاحظ في السطور من 27 إلى 29 أن هذه هي الطريقة التي يمكننا من خلالها استرداد المنشورات ذات الصلة، حيث نريد الحصول على المنشورات التي يكون لها الوسوم نفسها. قد تبدو سلسلة هذه التوابع مخيفة نوعًا ما، لكن لا تقلق بشأن ذلك، حيث سنوضّحها واحدًا تلو الآخر كما يلي: يعيد التابع الأول where('is_published', true) جميع المنشورات التي نشرناها. تتعقد الأمور بعض الشيء عند التابع whereHas()، حيث إذا أردنا فهم هذا التابع، فيجب أن نتحدث أولًا عن التابع has()، وهو تابع Laravel Eloquent الذي يسمح لنا بالتحقق من وجود علاقة كما في المثال التالي: $posts = Post::has('comments', '>', 3)->get(); ستسترد الشيفرة البرمجية السابقة جميع المنشورات التي تحتوي على أكثر من 3 تعليقات، ولاحظ أنه لا يمكنك استخدام where() لإنجاز ذلك، لأن comments ليس عمودًا في الجدول posts، بل هو جدول آخر له علاقة بالجدول posts. يعمل التابع whereHas() مثل التابع has() تمامًا، ولكنه يوفر مزيدًا من القوة، فالمعامل الثاني هو دالة تسمح بفحص محتوى جدول آخر، وهو الجدول tags في حالتنا، ويمكننا الوصول إلى الجدول tags من خلال المتغير $q. يأخذ التابع whereIn() في السطر 28 معاملَين هما: الأول هو العمود المحدَّد، والثاني هو مصفوفة من القيم المقبولة. يعيد هذا التابع السجلات ذات القيم المقبولة فقط ويستبعد الباقي. يجب أن تكون بقية التوابع سهلة الفهم، حيث يستبعد التابع where('id', '!=', $post->id) المنشور الحالي، ويأخذ التابع take(3) السجلات الثلاثة الأولى. يكون عرض المنشور المقابل resources/views/post.blade.php كما يلي: @extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3"> <img class="rounded-md object-cover h-96 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /> <h1 class="mt-5 mb-2 text-center text-2xl font-bold">{{ $post->title }}</h1> <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ $post->user->name }} | {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}</p> <div>{!! $post->content !!}</div> <div class="my-5"> @foreach ($post->tags as $tag) <a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" mr-3">#{{ $tag->name }}</a> @endforeach </div> <hr> <!-- المنشورات ذات الصلة --> <div class="grid grid-cols-3 gap-4 my-5"> @foreach ($related_posts as $post) <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }} </div> <h2 class="text-lg font-bold">{{ $post->title }}</h2> <p class="text-base"> {{ Str::limit(strip_tags($post->content), 150, '...') }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a> </div> </div> @endforeach </div> </div> @include('vendor.sidebar') </div> @endsection يُعَد {!! $post->content !!} في السطر 15 هو الوضع الآمن في لارافيل، حيث يخبر لارافيل بعرض وسوم HTML بدلًا من عرضها كنص عادي. صفحة البحث ضع ما يلي في ملف المتحكم app/Http/Controllers/PostController.php: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * عرض نتيجة البحث */ public function search(Request $request): View { $key = $request->input('q'); $posts = Post::where('title', 'like', "%{$key}%")->orderBy('id', 'desc')->paginate(env('PAGINATE_NUM')); $categories = Category::all(); $tags = Tag::all(); return view('search', [ 'key' => $key, 'posts' => $posts, 'categories' => $categories, 'tags' => $tags, ]); } . . . } يكون عرض البحث المقابل resources/views/search.blade.php كما يلي: @extends('layout') @section('title') <title>Search - {{ $key }}</title> @endsection @section('content') <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div> @include('vendor.sidebar') </div> @endsection ترجمة -وبتصرُّف- للمقال Laravel for Beginners #5 - Create the Frontend لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الرابع: إنشاء لوحة تحكم لموقع مدونة بسيطة تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder
-
تُستخدَم المصفوفات في العديد من المجالات لتمثيل كلٍّ من الكائنات والعمليات التي تُطبَّق عليها؛ إذ تُمثَّل النقاط والأشعة بالمصفوفات العمودية في الرسوميات الحاسوبية، وتُمثَّل العمليات عليها بمصفوفات مربّعة. سنناقش في هذا المقال المواضيع التالية: تعريف المصفوفة. عناصر المصفوفة. أبعاد المصفوفة. أسماء المصفوفات. تَساوي مصفوفتين. المصفوفة المربعة. عملية جمع المصفوفات وقواعدها. المصفوفة الصفرية. معاكس مصفوفة. طرح المصفوفات. ضرب المصفوفات بعدد حقيقي Scalar. منقول Transpose المصفوفة. تعريف المصفوفة تتكوّن المصفوفة العمودية من عمود واحد من الأعداد، ولكن يمكن ترتيب هذه الأعداد في عدة صفوف وعدة أعمدة، وبالتالي يمكن وضع الأعداد في مصفوفة ثنائية الأبعاد. إذًا المصفوفة هي مجموعة من الأعداد المرتَّبة ضمن عددٍ ثابت من الصفوف والأعمدة، وتكون هذه الأعداد أعدادًا حقيقية. يمكن أن تحتوي المصفوفات على أعداد معقدة أيضًا، ولكننا لن نراها في هذا المقال. إليك مثالًا لمصفوفة مكونة من ثلاثة صفوف وثلاثة أعمدة: الصف العلوي هو الصف 1، والعمود الموجود في أقصى اليسار هو العمود 1، والمصفوفة السابقة هي مصفوفة بحجم 3x3، لأنها تحتوي على ثلاثة صفوف وثلاثة أعمدة، ويكون التنسيق لوصف المصفوفات هو: rows X columns. يسمّى كل عدد يشكّل المصفوفة عنصرًا Element من عناصرها، ويكون للعناصر مواقع مُحدَّدة في المصفوفة؛ إذ تمثّل الزاويةُ العلوية اليسرى من المصفوفة الصفَّ 1 والعمود 1، وقيمة العنصر الموجود في الصف 1 والعمود 1 في المصفوفة السابقة هي 1، وقيمة العنصر الموجود في الصف 2 والعمود 3 هي 4.6، وقيمة العنصر في الصف 3 والعمود 1 هي 4. أبعاد المصفوفة يُطلَق مصطلح أبعاد المصفوفة على عدد صفوفها وأعمدتها. إليك فيما يلي مصفوفةً مكونةً من ثلاثة صفوف وعمودين: تُكتَب أحيانًا الأبعاد إلى جانب المصفوفة كما هو الحال في المصفوفة السابقة، ولكنه مجرد تذكير صغير وليس جزءًا من المصفوفة. إليك مثالًا آخر لمصفوفة ذات أبعاد مختلفة تتكون من صفين وثلاثة أعمدة، والتي تُعَد "نوع بيانات" مختلف عن المصفوفة السابقة: المصفوفة المربعة Square Matrix تحتوي المصفوفة المربعة Square Matrix على العدد نفسه من الصفوف والأعمدة، وتُستخدَم المصفوفات المربعة لتمثيل التحويلات الهندسية Transformations في الرسوميات الحاسوبية. المصفوفة المستطيلة Rectangular Matrix هي المصفوفة التي يمكن ألّا يكون لها العدد نفسه من الصفوف والأعمدة. مع ذلك، تطالب بعض المراجع بوجوب أن يكون عدد الصفوف مختلفًا عن عدد الأعمدة. أما المصفوفة العمودية Column Matrix فتتكون من عمود واحد، وهي مصفوفة بحجم N x 1، وقد استخدمنا المصفوفة العمودية في هذه السلسلة من المقالات لتمثيل الأشعة الهندسية مثل معظم كتب الرسوميات الحاسوبية. إليك مثالًا عن مصفوفة عمودية بحجم :4 x 1 تتكون المصفوفة السطرية Row Matrix من صفٍ واحد، حيث تستخدم بعض الكتب المصفوفات السطرية لتمثيل الأشعة الهندسية. تسمّي بعض الكتب المصفوفة العمودية بالشعاع العمودي وتسمّي المصفوفة السطرية بالشعاع السطري، ويُعَد ذلك أمرًا جيدًا، ولكنه غامضٌ بعض الشيء، لذا سنستخدم في هذا المقال مصطلح شعاع Vector للتعبير عن كائن هندسي، وسنستخدم المصفوفات العمودية لتمثيل الأشعة، كما سنستخدمها أيضًا لتمثيل النقاط الهندسية. أسماء المصفوفات إذا نسيت ما إذا كانت الصفوف أو الأعمدة تأتي أولًا، فتذكر فقط مصطلح الأعمدة الرومانية ROWman COLumns. يمكن إعطاء اسمٍ للمصفوفة، ويكون هذا الاسم عادةً حرفًا إنجليزيًا كبيرًا بخط عريض في النصوص المطبوعة مثل A أو M، وتُكتَب في بعض الأحيان الأبعاد على يمين الحرف كما في المصفوفة B3x3. لعناصر المصفوفة أسماءٌ أيضًا؛ إذ يكون اسم العنصر عادةً بحرفٍ مماثل لاسم المصفوفة، ولكنه حرفٌ صغير مع كتابة موضع العنصر بصورة منخفضة، لذلك يمكن مثلًا كتابة المصفوفة A التي أبعادها 3x3 على النحو التالي: يمكن أيضًا كتابة المصفوفة A = [aij] للقول بأن عناصر المصفوفة A تسمَّى aij. تساوي مصفوفتين إذا احتوت مصفوفتان على القيم نفسها، ولكن في مواقع مختلفة، فلا يمكن عَدّ هاتين المصفوفتين متساويتين، فلكي تكون المصفوفتان متساويتان، يجب أن يكون: لهما الأبعاد نفسها. يجب أن تكون العناصر المتقابلة متساوية. لنفترض أن لدينا المصفوفتين A n x m = [aij] و B p x q = [bij]، وعندها يكون A = B إذا وفقط إذا كان n=p و m=q وaij=bij لجميع قيم i و j ضمن المجال المحدَّد. إليك مصفوفتين غير متساويتين بالرغم من أنهما تحتويان على العناصر نفسها: كما أن المصفوفتين التاليتين غير متساويتين، لأن عناصرهما المتقابلة غير متساوية بالرغم من لهما الأبعاد نفسها: جمع المصفوفات إذا كانت لدينا مصفوفتان لهما العدد نفسه من الصفوف والأعمدة، فيمكن حساب مجموعهما؛ حيث إذا كانت A مصفوفةً أبعادها MxN، وكانت B مصفوفةً أبعادها MxN، فإن مجموعهما هو مصفوفة أبعادها MxN مكوّنة من جمع العناصر المتقابلة للمصفوفتين A و B. إليك مثال عن جمع مصفوفتين: تكون عناصر المصفوفات أعدادًا حقيقية ذات كسور عشرية في معظم الحالات العملية، وليست أعدادًا صحيحة صغيرة كتلك المستخدمة غالبًا في أمثلتنا. المصفوفة الصفرية Zero Matrix يمكن جمع مصفوفة أبعادها 3x2 مع مصفوفة أخرى أبعادها 3x2 دون تغيير المصفوفة الثانية إذا كانت جميع عناصر المصفوفة الأولى أصفارًا، والتي تُسمَّى بالمصفوفة الصفرية. إليك مثال عن مصفوفة صفرية أبعادها 3x3: اسم المصفوفة الصفرية هو صفر مكتوب بخط عريض 0، بالرغم من أننا قد ننسى في بعض الأحيان كتابته بخط عريض. أوجد ناتج عملية الجمع التالية: المجموع بالطبع هو المصفوفة غير الصفرية نفسها. قواعد جمع المصفوفات سنوضّح فيما يلي قواعد جمع المصفوفات، إذ يفترض أن تكون للمصفوفات الأبعاد نفسها: A + B = B + A A + 0 = 0 + A = A 0 + 0 = 0 تشبه هذه القواعد بعض القواعد الخاصة بجمع الأعداد الحقيقية، ولكن انتبه إلى أنه ليست جميع قواعد رياضيات المصفوفات مماثلة لتلك الخاصة برياضيات الأعداد الحقيقية. تنص القاعدة الأولى على أن جمع المصفوفات هو عملية تبديلية Commutative، بسبب إجراء عملية الجمع العادي على العناصر المتقابلة في المصفوفتين، ويُعَد الجمع العادي (الحقيقي) تبديليًا. كما يُعَد جمع المصفوفات عملية تجميعية Associative، وهي قاعدة أخرى تماثل قواعد رياضيات الأعداد الحقيقية، بسبب إجراء عملية الجمع العادية على العناصر المتقابلة من المصفوفتين: (A + B) + C = A + (B + C) لنوجِد ناتج جمع المصفوفتين التاليتين: لاحظ أن كل عنصر من عناصر المصفوفة الناتجة التي أبعادها 3x3 هو 10. ضرب المصفوفة بعدد حقيقي Scalar يمكن ضرب المصفوفة بعدد حقيقي (مقدار سلمي) من خلال ضرب كل عنصر من عناصر المصفوفة بهذا العدد. إليك مثال على ذلك، بحيث يكون المتغير a عددًا حقيقيًا: يكون كل عنصر في النتيجة معاكسًا للعنصر الأصل إذا كانت قيمة العدد الحقيقي a في المثال السابق تساوي -1، كما سنرى في الفقرة التالية. معاكس المصفوفة يمكن تشكيل معاكس للمصفوفة من خلال تغيير إشارة كل عنصر من عناصر هذه المصفوفة: -A = -1 A كما في المثال التالي: تنتج المصفوفة الصفرية عن جمع المصفوفة مع معاكسها. لاحظ أن الصفر الأخير هو صفر بخط عريض، والذي يعبّر عن المصفوفة الصفرية: A + (-A) = 0 ملاحظة: يمكن تعريف عملية الطرح مصفوفتين على أنها عملية جمع المصفوفة الأولى مع معاكس المصفوفة الثانية. طرح المصفوفات إذا كانت المصفوفتان A و B لهما العدد نفسه من الصفوف والأعمدة، فيمكن تعريف عملية الطرح A - B بأنها A + (-B)؛ إذ يمكن حساب A - B من خلال طرح العنصر المقابل من المصفوفة B من كل عنصر من عناصر المصفوفة A. إليك مثال غير مكتمل عن طرح المصفوفات: لاحظ العناصر الموجودة في الصف الأول من الإجابة السابقة؛ إذ تُعَد طريقة حساب النتيجة للعناصر الموجودة في الصف 1 والعمود 2 مربكةً بعض الشيء. لنحاول الآن وضع الإجابة مكان علامتي الاستفهام في المثال السابق كما يلي: منقول مصفوفة Transpose منقول المصفوفة هو مصفوفة جديدة تكون صفوفها هي أعمدة المصفوفة الأصلية، مما يؤدي أيضًا إلى جعل أعمدة المصفوفة الجديدة هي صفوف المصفوفة الأصلية. إليك مثال عن مصفوفة ومنقولها: الحرف المرتفع "T" يعني المنقول "Transpose". هناك طريقة أخرى للنظر إلى المنقول، وهي أن العنصر الموجود في الصف r والعمود c في المصفوفة الأصلية يُوضَع في الصف c والعمود r من المنقول؛ إذ يصبح العنصرُ arc في المصفوفة الأصلية هو العنصر acr في المصفوفة المنقولة. نتعامل عادةً مع المصفوفات المربعة لإيجاد منقولها، ولكن يمكن نقل المصفوفات غير المربعة أيضًا مثل المصفوفة التالية: إليك مثال آخر عن كيفية إيجاد منقول مصفوفة غير مربعة: قاعدة خاصة بمنقول المصفوفة إذا أوجدنا المنقول لمنقول المصفوفة نفسها، فستحصل على المصفوفة الأصلية مرةً أخرى كما في المثال التالي: ويوضح المثال السابق القاعدة: (AT)T = A. لاحظ أن منقول مصفوفة سطرية هو مصفوفة عمودية، وبالتالي فإن منقول مصفوفة عمودية هو مصفوفة سطرية كما في المثال التالي: المصفوفة المتناظرة Symmetric Matrix المصفوفة المتناظرة هي المصفوفة التي تتساوى مع منقولها، أي: AT = A، ولكن يجب أن تكون مصفوفةً مربعة. إليك فيما يلي مثال عن بعض المصفوفات المتناظرة: إذا كانت المصفوفة متناظرة، فستكون عناصرها aij = aji. ملاحظة: تُعَد المصفوفة الصفرية مصفوفةً متناظرةً أيضًا. قطر المصفوفة الرئيسي Main Diagonal يتكوّن القطر الرئيسي للمصفوفة من العناصر التي تقع على القطر الذي يمتد من أعلى يسار المصفوفة إلى أسفل يمينها، فإذا كانت لدينا المصفوفة A مثلًا، فإن عناصر قطرها الرئيسي هي العناصر التي يتساوى فيها رقم الصف مع رقم العمود ajj. لا يُعَد القطر الآخر للمصفوفة مهمًا وليس له اسم. ملاحظة: عناصر المصفوفة A التي لا تتغير عند تشكيل المنقول AT، هي العناصر الموجودة على طول القطر الرئيسي، لأن العناصر ajj في المصفوفة A هي العناصر نفسها في المنقول AT. الخلاصة إليك بعض القواعد التي ناقشناها في هذا المقال؛ إذ يجب عليك فهمها بدلًا من حفظها، حيث تحتوي المصفوفات في كل قاعدة من القواعد التالية على العدد نفسه من الصفوف والأعمدة: A + 0 = A A + B = B + A 0 + 0 = 0 A + (B + C) = (A + B) + C (ab) A = a(b A) a(A + B) = a B + a A a 0 = 0 (-1) A = -A A - A = 0 (AT)T = A 0T = 0 حيث أن a و b هي مقادير سلمية (أعداد حقيقية)، و A و B مصفوفتان، و 0 هي المصفوفة الصفرية ذات البعد المناسب. ملاحظة: إذا كان A = B و B = C، فإن A = C. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على المصفوفات وعملياتها البسيطة، وسنناقش في المقال التالي مزيدًا من الأمور التي يمكن تطبيقها باستخدام المصفوفات. ترجمة -وبتصرُّف- للفصل Matrices and Simple Matrix Operations من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: الجداء الشعاعي في التصاميم ثلاثية الأبعاد وخاصياته وحسابه كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد.
-
سننشئ في هذا المقال تطبيق مدونة باستخدام لارافيل ونجعل تطبيقنا كامل الميزات ويحتوي على منشورات وفئات Categories ووسوم Tags، حيث ناقشنا في المقال السابق عمليات CRUD للمنشورات، وسنكرر اليوم هذه العمليات نفسها على كل من الفئات والوسوم، كما سنناقش كيفية التعامل مع العلاقات فيما بينها. تهيئة مشروع لارافيل جديد سنبدأ بمشروع لارافيل جديد، حيث سننشئ مجلد العمل وننتقل إليه، ولكن تأكّد من تشغيل دوكر Docker، ثم نفّذ الأمر التالي: curl -s https://laravel.build/<app_name> | bash انتقل إلى مجلد التطبيق وابدأ تشغيل الخادم كما يلي: cd <app_name> ./vendor/bin/sail up لننشئ اسمًا بديلًا للأداة sail لتسهيل الأمور، لذا شغّل الأمر التالي: alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail' يمكنك من الآن فصاعدًا تشغيل الأداة Sail مباشرةً دون تحديد المسار بأكمله كما يلي: sail up استيثاق المستخدم User authentication يأتي إطار عمل لارافيل مع نظام بيئي كبير فهو يحتوي على العديد من الأدوات والموارد الإضافية التي تسهل على المطورين برمجة التطبيقات، وتُعَد حزمة Breeze من لارافيل جزءًا من هذا النظام، فهي توفر طريقة سريعة لإعداد استيثاق المستخدم وتسجيله في تطبيق لارافيل. تتضمّن حزمة Breeze عروضًا Views ومتحكّمات Controllers مبنية مسبقًا خاصة بالاستيثاق، بالإضافة إلى مجموعة من واجهات برمجة التطبيقات الخلفية للتعامل مع استيثاق المستخدم وتسجيله، وصُمِّمت هذه الحزمة لتكون سهلة التثبيت والضبط مع وجود الحد الأدنى من الإعداد المطلوب. استخدم الأوامر التالية لتثبيت حزمة Breeze من لارافيل: sail composer require laravel/breeze --dev sail artisan breeze:install sail artisan migrate sail npm install sail npm run dev ستولّد هذه العملية تلقائيًا كل من المتحكمات والبرمجيات الوسيطة والعروض المطلوبة اللازمة لإنشاء نظام استيثاق مستخدم أساسي في تطبيقك. يمكنك الوصول إلى صفحة التسجيل من العنوان http://127.0.0.1/register، ثم تسجّل حسابًا جديدًا وسيُعاد توجيهك إلى لوحة التحكم. لن نناقش في هذا المقال كيفية عمل نظام استيثاق المستخدم، لأنه يرتبط ببعض المفاهيم المتقدمة إلى حدٍ ما، ولكن يوصَى بشدة بإلقاء نظرة على الملفات التي تولّدت هنا، فهي توفر نظرة أعمق حول كيفية العمل في لارافيل. إعداد قاعدة البيانات يجب بعد ذلك أن يكون لدينا تصور حول كيفية ظهور تطبيق المدونة، حيث نحتاج أولًا إلى قاعدة بيانات يمكنها تخزين المنشورات والفئات والوسوم، وسيكون لكل جدول قاعدة بيانات البنيةُ التالية: جدول المنشورات Posts: المفتاح نوعه id عدد صحيح كبير BigInteger created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد title سلسلة نصية String cover سلسلة نصية String content نص Text is_published قيمة منطقية Boolean جدول الفئات Categories: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد name سلسلة نصية String جدول الوسوم Tags: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد name سلسلة نصية يجب أن يكون هناك أيضًا جدول للمستخدمين users، ولكن لاحاجة لأن ننشئه فقد ولّدته حزمة Breeze من لارافيل مسبقًا، لذا سنتخطى هذه الخطوة حاليًا. يكون لهذه الجداول علاقات مع بعضها البعض كما هو موضح فيما يلي: كل مستخدم لديه منشورات متعددة. كل فئة لديها العديد من المنشورات. كل وسم لديه العديد من المنشورات. يعودة كل منشور إلى مستخدم واحد. يعود كل منشور إلى فئة واحدة. كل منشور له العديد من الوسوم. يمكننا إنشاء هذه العلاقات، ولكن يجب تعديل جدول المنشورات كما يلي: جدول المنشورات مع العلاقات: المفتاح نوعه id عدد صحيح كبير created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد title سلسلة نصية String cover سلسلة نصية String content نص Text is_published قيمة منطقية user_id عدد صحيح كبير category_id عدد صحيح كبير ونحتاج أيضًا إلى جدول منفصل لعلاقة المنشور مع الوسم كما يلي: جدول يمثل علاقة المنشور مع الوسم Post/Tag: المفتاح نوعه post_id عدد صحيح كبير tag_id عدد صحيح كبير تطبيق بنية قاعدة البيانات يمكن تطبيق التصميم السابق من خلال توليد النماذج Models وملفات التهجير Migration باستخدام الأوامر التالية: sail artisan make:model Post --migration sail artisan make:model Category --migration sail artisan make:model Tag --migration بالإضافة إلى توليد ملف تهجير منفصل للجدول post_tag باستخدام الأمر التالي: sail artisan make:migration create_post_tag_table وسينشأ ملف التهجير database/migrations/create_posts_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->string('cover'); $table->text('content'); $table->boolean('is_published'); $table->bigInteger('user_id'); $table->bigInteger('category_id'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } }; وسينشأ ملف التهجير database/migrations/create_categories_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('name'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('categories'); } }; وسينشأ ملف التهجير database/migrations/create_tags_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('tags', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('name'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('tags'); } }; وسينشأ أيضُا ملف التهجير database/migrations/create_post_tag_table.php التالي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('post_tag', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->bigInteger('post_id'); $table->bigInteger('tag_id'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('post_tag'); } }; طبّق هذه التغييرات باستخدام الأمر التالي: sail artisan migrate يجب بعد ذلك تفعيل الإسناد الجماعي Mass Assignment لحقول محدَّدة بالنسبة للنماذج المقابلة حتى نتمكّن من استخدام تابعَي create أو update معها كما ناقشنا في المقال السابق، ويجب أيضًا تعريف العلاقات بين جداول قاعدة البيانات. سيكون ملف النموذج app/Models/Post.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Post extends Model { use HasFactory; protected $fillable = [ "title", 'content', 'cover', 'is_published' ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } } وسيكون ملف النموذج app/Models/Category.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Category extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } ويكون ملف النموذج app/Models/Tag.php كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Tag extends Model { use HasFactory; protected $fillable = [ 'name', ]; public function posts(): BelongsToMany { return $this->belongsToMany(Post::class); } } ويكون ملف النموذج app/Models/User.php كما يلي: <?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; /** * السمات Attributes التي نسندها إسنادًا جماعيًا * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', ]; /** * السمات التي يجب أن تكون مخفية لعملية السَلسلة Serialization * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', ]; /** * السمات التي يجب تغيير نوعها Cast * * @var array<string, string> */ protected $casts = [ 'email_verified_at' => 'datetime', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } المتحكمات Controllers والوِجهات Routes نحتاج إلى إنشاء متحكم موارد واحد لكل مورد (منشور وفئة ووسم) باستخدام الأوامر التالية: php artisan make:controller PostController --resource php artisan make:controller CategoryController --resource php artisan make:controller TagController --resource ننشئ بعد ذلك وِجهات لكل من هذه المتحكمات في الملف routes/web.php كما يلي: <?php use App\Http\Controllers\CategoryController; use App\Http\Controllers\PostController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\TagController; use Illuminate\Support\Facades\Route; // وِجهات لوحة التحكم Route::prefix('dashboard')->group(function () { // الصفحة الرئيسية للوحة التحكم Route::get('/', function () { return view('dashboard'); })->name('dashboard'); // مورد الفئة للوحة التحكم Route::resource('categories', CategoryController::class); // مورد الوسم للوحة التحكم Route::resource('tags', TagController::class); // مورد المنشور للوحة التحكم Route::resource('posts', PostController::class); })->middleware(['auth', 'verified']); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); require __DIR__ . '/auth.php'; لاحظ تجميع جميع الوِجهات باستخدام البادئة /dashboard، ويكون للمجموعة البرمجيةُ الوسيطة auth، مما يعني أنه يجب على المستخدم تسجيل الدخول للوصول إلى لوحة التحكم. متحكمات الفئة والوسم Category/Tag يُعََد المتحكمان CategoryController و TagController واضحَين إلى حدٍ ما، ويمكنك إعدادهما بالطريقة نفسها التي أنشأنا بها المتحكم PostController في المقال السابق. إذًا لننشئ أولًا متحكم الفئة app/Http/Controllers/CategoryController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Category; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class CategoryController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $categories = Category::all(); return view('categories.index', [ 'categories' => $categories ]); } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('categories.create'); } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $category = new Category(); $category->name = $name; // حفظ البيانات $category->save(); return redirect()->route('categories.index'); } /** * عرض المورد المُحدَّد */ public function show(string $id): View { $category = Category::all()->find($id); $posts = $category->posts(); return view('categories.show', [ 'category' => $category, 'posts' => $posts ]); } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $category = Category::all()->find($id); return view('categories.edit', [ 'category' => $category ]); } /** * تحديث المورد المُحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // البحث عن الفئة المطلوبة ووضع البيانات المطلوبة في العمود المقابل $category = Category::all()->find($id); $category->name = $name; // حفظ البيانات $category->save(); return redirect()->route('categories.index'); } /** * إزالة المورد المٌحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $category = Category::all()->find($id); $category->delete(); return redirect()->route('categories.index'); } } ولننشئ الآن متحكم الوسم app/Http/Controllers/TagController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class TagController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $tags = Tag::all(); return view('tags.index', [ 'tags' => $tags ]); } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('tags.create'); } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $tag = new Tag(); $tag->name = $name; // حفظ البيانات $tag->save(); return redirect()->route('tags.index'); } /** * عرض المورد المُحدَّد */ public function show(string $id): View { $tag = Tag::all()->find($id); $posts = $tag->posts(); return view('tags.show', [ 'tag' => $tag, 'posts' => $posts ]); } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $tag = Tag::all()->find($id); return view('tags.edit', [ 'tag' => $tag ]); } /** * تحديث المورد المُحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $name = $request->input('name'); // البحث عن الوسم المطلوب ووضع البيانات المطلوبة في العمود المقابل $tag = Tag::all()->find($id); $tag->name = $name; // حفظ البيانات $tag->save(); return redirect()->route('tags.index'); } /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $tag = Tag::all()->find($id); $tag->delete(); return redirect()->route('tags.index'); } } تذكّر أنه يمكنك التحقق من اسم الوِجهات باستخدام الأمر التالي: sail artisan route:list متحكم المنشور Post يُعَد متحكم المنشور PostController أكثر تعقيدًا بعض الشيء، إذ يجب عليك التعامل مع عمليات رفع الصور والعلاقات بين الجداول في قاعدة البيانات في التابع store(). إذًا لننشئ هذا المتحكم app/Http/Controllers/PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use App\Models\Tag; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; class PostController extends Controller { . . . /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); if ($request->input('is_published') == 'on') { $is_published = true; } else { $is_published = false; } // إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post(); $post->title = $title; $post->content = $content; $post->is_published = $is_published; // حفظ صورة الغلاف $path = $request->file('cover')->store('cover', 'public'); $post->cover = $path; // ضبط المستخدم $user = Auth::user(); $post->user()->associate($user); // ضبط الفئة $category = Category::find($request->input('category')); $post->category()->associate($category); // حفظ المنشور $post->save(); // ضبط الوسوم $tags = $request->input('tags'); foreach ($tags as $tag) { $post->tags()->attach($tag); } return redirect()->route('posts.index'); } . . . } هناك بعض الأشياء التي يجب ملاحظتها في التابع store()، حيث سنستخدم في الأسطر من 28 إلى 32 مربع اختيار HTML لتمثيل الحقل is_published، وتكون قيمته إما 'on' أو null، ولكن تُحفَظ قيمته في قاعدة البيانات بوصفها true أو false، لذلك يجب استخدام التعليمة if لحل هذه المشكلة. يمكننا استرداد الملفات في السطور من 41 إلى 42 من خلال استخدام التابع file() بدلًا من التابع input()، ويُحفظ الملف في القرص public ضمن المجلد cover. نحصل في السطور من 45 إلى 46 على المستخدم الحالي باستخدام التابع Auth::user()، ونربط المنشور بالمستخدم باستخدام التابع associate()، وتفعَل السطور من 49 إلى 50 الشيء نفسه بالنسبة للفئة، وتذكّر أنه يمكنك تطبيق ذلك فقط مع المتغير $post وليس $user أو $category، لأن العمودان user_id و category_id موجودان في الجدول posts. وأخيرًا، يجب حفظ المنشور الحالي في قاعدة البيانات، ثم استرداد قائمة الوسوم وإرفاق كل منها بالمنشور واحدًا تلوَ الآخر باستخدام التابع attach() بالنسبة للوسوم كما هو موضح في السطور من 56 إلى 60. تجري الأمور بطريقة مشابهة بالنسبة إلى التابع update()، باستثناء أنه يجب إزالة جميع الوسوم الموجودة قبل أن تتمكّن من إرفاق الوسوم الجديدة كما يلي: $post->tags()->detach(); العروض Views تذكر دائمًا أن تكون منظّمًا عند إنشاء نظام عرض، حيث سنتّبع البنية التالية في مثالنا: resources/views ├── auth ├── categories │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── components ├── layouts ├── posts │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── profile ├── tags │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── dashboard.blade.php └── welcome.blade.php أنشأنا ثلاثة مجلدات هي: posts و categories و tags، ولكل منها أربعة قوالب هي: create و edit و index و show باستثناء المجلد posts، لأن وجود صفحة show للمنشورات في لوحة التحكم أمر غير ضروري. يؤدي تضمين جميع هذه العروض في مقال واحد إلى جعل هذا المقال طويلًا بلا داعٍ، لذا سنوضّح فقط صفحات إنشاء وتعديل وفهرس المنشورات، ولكن يمكنك الاطلاع على الشيفرة المصدرية الكاملة على Github. عرض إنشاء منشور لننشئ أولًا عرض إنشاء المنشور resources/views/posts/create.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin" ></script> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class=""> <form action="{{ route('posts.store') }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data" > {{ csrf_field() }} <input type="checkbox" name="is_published" id="is_published" /> <x-input-label for="is_published" >Make this post public</x-input-label > <br /> <x-input-label for="title">{{ __('Title') }}</x-input-label> <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" /> <br /> <x-input-label for="content">{{ __('Content') }}</x-input-label> <textarea name="content" id="content" cols="30" rows="30" ></textarea> <br /> <x-input-label for="cover">{{ __('Cover Image') }}</x-input-label> <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" required autofocus autocomplete="cover" /> <br /> <x-input-label for="category">{{ __('Category') }}</x-input-label> <select id="category" name="category"> @foreach($categories as $category) <option value="{{ $category->id }}">{{ $category->name }}</option> @endforeach </select> <br /> <x-input-label for="tags">{{ __('Tags') }}</x-input-label> <select id="tags" name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}">{{ $tag->name }}</option> @endforeach </select> <br /> <x-primary-button>{{ __('Save') }}</x-primary-button> </form> <script> tinymce.init({. . .}); </script> </div> </div> </div> </div> </x-app-layout> استخدمنا في مثالنا محرّر النصوص TinyMCE، ولكن يمكنك استخدام محرّر نصوص آخر عوضًا عنه، أو استخدم العنصر <textarea></textarea> إذا أدرتَ ذلك. يجب أن تحتوي الاستمارة الموجودة في السطر 24 على السمة التي هي enctype="multipart/form-data" لأننا لا ننقل النصوص فحسب، بل توجد ملفات أيضًا. تذكر في السطر 59 استخدام السمة type="file" لأننا نرفع صورة، وستُنقَل قيمة الخيار في السطور من 67 إلى 71 إلى الواجهة الخلفية. هناك شيئان يجب الانتباه إليهما في الأسطر من 74 إلى 78، فلاحظ أولًا السمة name="tags[]"، حيث تخبر هذه الأقواس [] لارافيل بنقل مصفوفة قابلة للتكرار بدلًا من النصوص. ثانيًا، تنشئ السمة multiple استمارة متعددة التحديد بدلًا من تحديد فردي مثل استمارة الفئات. عرض تعديل المنشور لننشئ الآن العرض resources/views/posts/edit.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin" ></script> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg"> <div class=""> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data" > {{ csrf_field() }} {{ method_field('PUT') }} <input type="checkbox" name="is_published" id="is_published" @checked($post- />is_published)/> <x-input-label for="is_published" >Make this post public</x-input-label > <br /> <x-input-label for="title">{{ __('Title') }}</x-input-label> <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" value="{{ $post->title }}" /> <br /> <x-input-label for="content">{{ __('Content') }}</x-input-label> <textarea name="content" id="content" cols="30" rows="30"> {{ $post->content }}</textarea > <br /> <x-input-label for="cover" >{{ __('Update Cover Image') }}</x-input-label > <img src="{{ Illuminate\Support\Facades\Storage::url($post->cover) }}" alt="cover image" width="200" /> <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" autofocus autocomplete="cover" /> <br /> <x-input-label for="category">{{ __('Category') }}</x-input-label> <select id="category" name="category"> @foreach($categories as $category) <option value="{{ $category->id }}" @selected($post-> category->id == $category->id)>{{ $category->name }} </option> @endforeach </select> <br /> <x-input-label for="tags">{{ __('Tags') }}</x-input-label> <select id="tags" name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}" @selected($post-> tags->contains($tag))>{{ $tag->name }} </option> @endforeach </select> <br /> <x-primary-button>{{ __('Save') }}</x-primary-button> </form> <script> tinymce.init({. . .}); </script> </div> </div> </div> </div> </x-app-layout> لاحظ في السطور من 24 إلى 26 أن لغة HTML لا تدعم التابع PUT افتراضيًا، لذا نستخدم السمة method="POST" ثم نخبر لارافيل باستخدام التابع PUT من خلال التعليمة {{ method_field('PUT') }}. عرض فهرس المنشورات لننشئ الآن العرض resources/views/posts/index.blade.php كما يلي: <x-app-layout> <x-slot name="header"> <div class="flex justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" > {{ __('Posts') }} </h2> <a href="{{ route('posts.create') }}"> <x-primary-button>{{ __('New') }}</x-primary-button> </a> </div> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> @foreach($posts as $post) <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-4 px-4 h-20 flex justify-between items-center" > <div class="text-gray-900 dark:text-gray-100"> <p>{{ $post->title }}</p> </div> <div class="space-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}"> <x-primary-button>{{ __('Edit') }}</x-primary-button></a > <form method="post" action="{{ route('posts.destroy', ['post' => $post->id]) }}" class="inline" > {{ csrf_field() }} {{ method_field('DELETE') }} <x-danger-button> {{ __('Delete') }} </x-danger-button> </form> </div> </div> @endforeach </div> </div> </x-app-layout> لاحظ أن زر الحذف ليس زرًا عاديًا، إذ يجب أن يكون استمارة مع تابع DELETE، لأن الرابط العادي يمتلك التابع GET فقط. يُفترَض الآن أن تكون قادرًا على إنشاء بقية نظام العرض بسهولة. لقطات الشاشة وأخيرًا، إليك بعض لقطات الشاشة للوحة التحكم التي أنشأناها: صفحة إدارة التصنيفات: ننتقل لصفحة الإدارة كما يلي: ثم نختار التصنيفات Categories من الصورة أعلاه لننتقل للصفحة التالية لإدارة التصنيفات: صفحة إنشاء تصنيف جديد: صفحة تحديث المنشور: تابع معنا المقال التالي والأخير من هذه السلسلة حيث سننشئ فيه كل ما يخص الواجهة الأمامية من التطبيق كي يتمكن المستخدمون من رؤية منشورات المدونة والتفاعل معها. ترجمة -وبتصرُّف- للمقال Laravel for Beginners #4 - Create a Dashboard لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الثالث: استخدام عمليات CRUD لإنشاء مدونة بسيطة تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel إنشاء استمارة اتصال في Laravel باستخدام ميزة Form Request
-
سنستخدم في هذا المقال كل ما تعلّمناه في المقالين السابقين من سلسلة مقالات لارافيل للمبتدئين لإنشاء مشروع حقيقي، حيث سننشئ مدونة صغيرة تحتوي على منشورات فقط بدون فئات Categories أو وسوم Tags، وكل منشور له عنوان ومحتوى، إذ لن ننشئ تطبيق مدونة كامل المواصفات، وسنوضّح كيفية استرداد البيانات من قاعدة البيانات، وكيفية إنشاء أو تحديث معلومات جديدة، وكيفية حفظها في قاعدة البيانات، وكيفية حذفها، حيث يمثّل الاختصار CRUD عمليات الإنشاء Create والقراءة Read والتحديث Update والحذف Delete. تصميم بنية قاعدة البيانات سننشئ أولًا ملف تهجير Migration للجدول post، ويجب أن يحتوي هذا الجدول على العنوان والمحتوى، ويمكنك توليد ملف تهجير باستخدام الأمر التالي: php artisan make:migration create_posts_table وسيتضمن الملف ما يلي: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->text('content'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } }; أنشأنا في هذا المثال خمسة أعمدة في الجدول post باستدعاء 5 توابع وهي: ينشئ التابع id() عمود المعرّف id الذي يُستخدَم عادة للفهرسة. ينشئ التابع timestamps() عمودين هما: created_at و uptated_at، وسيُحدَّث هذان العمودان تلقائيًا عند إنشاء السجل وتحديثه. ينشئ التابع string('title') العمود title الذي نوعه VARCHAR ويبلغ طوله الافتراضي 255 بايتًا. ينشئ التابع string('content') عمود المحتوى content. يمكنك تطبيق التغييرات من خلال تشغيل الأمر التالي: php artisan migrate ويجب أن ينشأ جدول posts جديد كما يلي: يمكننا الآن إنشاء النموذج Model المقابل لهذا الجدول باستخدام الأمر التالي: php artisan make:model Post وسيتضمّن هذا النموذج app/Models/Post.php ما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; } لننشئ الآن متحكم الموارد Resource Controller المقابل باستخدام الأمر التالي: php artisan make:controller PostController --resource وأخيرًا، سجّل هذا المتحكم في الموجِّه Router كما يلي: use App\Http\Controllers\PostController; Route::resource('posts', PostController::class); يمكنك التحقق من الوِجهات Routes المُسجَّلة من خلال تشغيل الأمر التالي: php artisan route:list وسيظهر الخرج التالي: GET|HEAD posts ................................................................. posts.index › PostController@index POST posts ................................................................. posts.store › PostController@store GET|HEAD posts/create ........................................................ posts.create › PostController@create GET|HEAD posts/{post} ............................................................ posts.show › PostController@show PUT|PATCH posts/{post} ........................................................ posts.update › PostController@update DELETE posts/{post} ...................................................... posts.destroy › PostController@destroy GET|HEAD posts/{post}/edit ....................................................... posts.edit › PostController@edit يحتوي الخرج السابق على معلومات مثل توابع الطلبات Request Methods وتوابع المتحكّمات Controller Methods بالإضافة إلى أسماء الوِجهات، وتُعَد هذه المعلومات مهمة جدًا وسنحتاج إليها لاحقًا في هذا المقال. عمليات CRUD حان الوقت الآن لنتعمّق في التطبيق نفسه، فمن غير المحتمل أن ننشئ جميع المتحكمات أولًا ثم نصمّم القوالب ثم ننتقل إلى الموجّهات عند إنشاء تطبيقات واقعية، إذ يجب أن نفكّر من وجهة نظر المستخدم وفي الإجراءات التي قد يرغب المستخدم في اتخاذها. يجب أن يكون لدى المستخدم القدرة على إجراء أربع عمليات لكل مورد، والذي هو Post في حالتنا، وهذه العمليات هي: الإنشاء Create: يجب أن يكون المستخدم قادرًا على إنشاء موارد جديدة وحفظها في قاعدة البيانات. القراءة Read: يجب أن يكون المستخدم قادرًا على قراءة الموارد سواءً من خلال استرداد قائمة الموارد أو التحقق من تفاصيل مورد محدّد. التحديث Update: يجب أن يكون المستخدم قادرًا على تحديث الموارد الموجودة وتحديث سجل قاعدة البيانات المقابلة لها. الحذف Delete: يجب أن يكون المستخدم قادرًا على إزالة مورد من قاعدة البيانات. ويشار إلى هذه العمليات مع بعضها البعض باسم عمليات CRUD. إجراء الإنشاء Create لا تزال قاعدة بياناتنا فارغة حاليًا، لذا قد يرغب المستخدم في إنشاء منشورات جديدة، فلنبدأ بإجراء الإنشاء (C)، حيث يمكن إكمال هذا الإجراء من خلال استخدام التابعين التاليين: تابع المتحكم create() الذي يعرض استمارة، مما يسمح للمستخدم بملء العنوان والمحتوى. تابع المتحكم store() الذي يحفظ المنشور الذي أنشأناه حديثًا في قاعدة البيانات، ويعيد توجيه المستخدم إلى صفحة القائمة. يطابق التابع create() نمط URL الذي هو /posts/create (التابع GET)، ويطابق التابع store() نمط URL الذي هو /post (التابع POST). إليك مراجعةً مختصرة لتوابع HTTP في حال احتياجك إلى تجديد بعض المعلومات: تابع GET هو تابع طلبات HTTP الأكثر استخدامًا، ويُستخدَم لطلب البيانات والموارد من الخادم. يُستخدَم تابع POST لإرسال البيانات إلى الخادم، ويُستخدَم لإنشاء أو تحديث المورد. يعمل تابع HEAD مثل تابع GET تمامًا، باستثناء أن استجابة HTTP ستحتوي على الترويسة فقط بدون الجسم، ويستخدم المطورون هذا التابع لأغراض تنقيح الأخطاء Debugging. يُعَد تابع PUT مشابهًا لتابع POST، مع اختلاف واحد بسيط، حيث إذا أرسلتَ باستخدام التابع POST موردًا موجودًا مسبقًا على الخادم، فلن يسبّب هذا الإجراء أيّ فرق، ولكن سيكرّر تابع PUT هذا المورد في كل مرة نقدّم فيها الطلب. يزيل تابع DELETE موردًا من الخادم. لنبدأ بالتابع create()، حيث سيتضمّن ملف المتحكم app/Http/Controllers/PostController.php ما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; . . . class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('posts.create'); } . . . } سيُنفّذ هذا التابع عندما ترسل طلب GET إلى /posts/create، وسيؤشّر إلى العرض views/posts/create.blade.php. لاحظ تغيير الاستجابة Response إلى عرض View في السطر 16، لأن هذه التوابع يجب أن تعيد عرضًا. يجب بعد ذلك أن ننشئ العرض المقابل، حيث سنبدأ بعرض تخطيط الصفحة layout، إذ سيتضمّن الملف views/layout.blade.php ما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.tailwindcss.com"></script> @yield('title') </head> <body class="container mx-auto font-serif"> <div class="bg-white text-black font-serif"> <div id="nav"> <nav class="flex flex-row justify-between h-16 items-center shadow-md"> <div class="px-5 text-2xl"> <a href="{{ route('posts.index') }}"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="{{ route('posts.create') }}" class="hover:underline hover:underline-offset-1" >New Post</a > <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > </div> </nav> </div> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </div> </body> </html> لاحظ في السطرين 16 و 20 وجود الأقواس المزدوجة المتعرجة {{ }} التي تسمح بتنفيذ شيفرة PHP البرمجية ضمن القالب، وبالتالي سيعيد التابع route('posts.index') الوِجهة التي اسمها posts.index، حيث يمكنك التحقق من أسماء الوِجهات من خلال الرجوع إلى خرج الأمر php artisan route:list. لننتقل بعد ذلك إلى العرض create، حيث يجب أن تكون منظمًا في هذا العرض. يُعَد العرض create مُخصّصًا للإجراءات المتعلقة بالمنشور، لذا أنشأنا المجلد post لتخزين هذا العرض. سيتضمن العرض views/posts/create.blade.php ما يلي: @extends('layout') @section('title') <title>Create</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2> <form action="{{ route('posts.store') }}" method="POST"> {{ csrf_field() }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" ></textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection توجد بعض الأشياء التي يجب ملاحظتها عند استخدام الاستمارات لنقل البيانات، وهي: السطر 6: تحدِّد سمة Attribute الإجراء action ما يحدث عند إرسال هذه الاستمارة، وتوجّه المتصفح في هذه الحالة لزيارة الوِجهة posts.store باستخدام تابع HTTP الذي هو POST. السطر 7 {{ csrf_field() }}: يُعَد CSRF هجومًا ضارًا يستهدف تطبيقات الويب، وتوفر الدالة csrf_field() الحماية من هذا النوع من الهجمات. اطلع على مقال تعرف على أمان مواقع الويب لمعرفة المزيد عن هجمات تزوير الطلبات عبر المواقع Cross-Site Request Forgery -أو CSRF اختصارًا. السمة name في السطر 12 و 20: يُربَط ما يدخله المستخدم بمتغير عند إرسال الاستمارة، وتحدّد السمة name اسم هذا المتغير، فمثلًا إذا كان name="title"، فسيُربَط دخل المستخدم بالمتغير title، ويمكن الوصول إلى قيمته باستخدام التعليمة $request->input('title')، حيث سنرى كيفية عملها لاحقًا. السطر 27: يجب ضبط السمة type على القيمة submit حتى تعمل هذه الاستمارة. شغّل الآن خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/posts/create حيث ستظهر الصفحة التالية: إذا نقرتَ على زر الإرسال "Submit"، فسيرسل المتصفح طلب POST إلى الخادم، وسيُنفَّذ التابع store() هذه المرة، وسيحتوي طلب POST على دخل المستخدم، ويمكن الوصول إليه كما يلي في الملف PostController.php: <?php namespace App\Http\Controllers; . . . class PostController extends Controller { . . . /** * تخزين مورد أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post; $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.index'); } . . . } سيُعاد توجيهك إلى الوِجهة posts.index بعد تخزين البيانات. عُد إلى متصفحك واكتب عنوانًا ومحتوًى جديدًا، ثم انقر على زر الإرسال. لم ننشئ العرض index بعد، لذا ستُعاد رسالة خطأ، ولكن إذا تحققتَ من قاعدة البيانات، فيجب وجود سجل جديد مضاف كما يلي: إجراء القائمة List لننتقل الآن إلى عملية القراءة Read، حيث يوجد نوعان مختلفان من عمليات القراءة هما: الأول هو إجراء القائمة الذي يعيد قائمةً بجميع المنشورات إلى المستخدم، والإجراء الثاني سنوضّحه في الفقرة التالية. يقابل إجراء القائمة التابعَ index() في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $posts = Post::all(); return view('posts.index', [ 'posts' => $posts, ]); } . . . } ويكون عرض index المقابل هو views/post/index.blade.php كما يلي: @extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> @foreach($posts as $post) <h2 class="text-2xl font-semibold underline mb-2"> <a href="{{ route('posts.show', ['post' => $post->id]) }}" >{{ $post->title }}</a > </h2> <p class="mb-4">{{ Illuminate\Support\Str::words($post->content, 100) }}</p> @endforeach </div> @endsection تتكرر حلقة foreach في السطر 5 على جميع المنشورات $posts المُسترَدة، ثم تسنِد كل قيمة إلى المتغير $post. لاحظ في السطر7 كيفية تمرير معرّف id المنشور إلى الوِجهة posts.show، ثم ستمرِّر هذه الوِجهة هذا المتغير إلى تابع المتحكم show() الذي سنراه لاحقًا. يُعَد التابع Str::words() في السطر 11 دالةً مساعدة في لغة PHP، وسيأخذ أول 100 كلمة من المحتوى content فقط. إجراء العرض Show عملية القراءة الثانية هي إجراء العرض الذي يعرض تفاصيل مورد معين، ويتحقّق هذا الإجراء باستخدام التابع show() في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض المورد المُحدَّد */ public function show(string $id): View { $post = Post::all()->find($id); return view('posts.show', [ 'post' => $post, ]); } . . . } ويكون عرض show المقابل هو views/post/show.blade.php كما يلي: @extends('layout') @section('title') <title>{{ $post->title }}</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-2">{{ $post->title }}</h2> <p class="mb-4">{{ $post->content }}</p> <div class="grid grid-cols-2 gap-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" >Update</a > <form action="{{ route('posts.destroy', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Delete </button> </form> </div> </div> @endsection هناك بعض الأشياء التي يجب أن نلاحظها في المثال السابق. أولًا، لاحظ كيف أن زر التحديث "Update" هو رابط بسيط، ولكن زر الحذف "Delete" هو استمارة، لأن الرابط سيخبر المتصفح بإرسال طلب GET إلى الخادم، بينما نحتاج شيئًا آخر لإجراء الحذف، حيث إذا نظرتَ داخل الاستمارة، فستجد أنها تحتوي على السمة method="POST"، ولكننا نحتاج التابع DELETE لإجراء الحذف. تدعم لغة HTML التابعين GET و POST افتراضيًا، وإذا كنت بحاجة شيء آخر، فيجب أن تضبط السمة method="POST"، وأن تستخدم التابع method_field() بدلًا من ذلك. إجراء التحديث Update تستخدم عملية التحديث التابع edit() الذي يعرض استمارة HTML، والتابع update() الذي يجري تغييرات على قاعدة البيانات، لذا ضع هذين التابعين في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $post = Post::all()->find($id); return view('posts.edit', [ 'post' => $post, ]); } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // البحث عن المنشور المطلوب ووضع البيانات المطلوبة في العمود المقابل $post = Post::all()->find($id); $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.show', ['post' => $id]); } . . . } ويكون عرض edit المقابل هو views/post/edit.blade.php كما يلي: @extends('layout') @section('title') <title>Edit</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Edit post</h2> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('PUT') }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" value="{{ $post->title }}" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" > {{ $post->content }}</textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection إجراء الحذف Delete وأخيرًا، يوجد التابع destroy() للحذف، إذًا لنضعه في الملف PostController.php كما يلي: <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $post = Post::all()->find($id); $post->delete(); return redirect()->route('posts.index'); } } لا يتطلب هذا الإجراء عرضًا، بما أنه يعيد توجيهك إلى posts.index بعد اكتمال الإجراء. ترجمة -وبتصرُّف- للمقال Laravel for Beginners #3 - The CRUD Operations لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الثاني: استخدام بنية MVC لإنشاء مدونة بسيطة تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel المبادئ الأساسيّة لإطار العمل Eloquent ORM أساسيات التحقق من المدخلات في Laravel كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel
-
تُعدّ لغة PHP من أكثر اللغات استخدامًا في تطوير مواقع الويب لأنها سهلة وتتيح بناء تطبيقات الويب بسرعة. لكن رغم سهولتها إلا أنها تطورت وأصبحت تضم العديد من المميزات والفروق الرئيسية والدقيقة التي يمكن أن يخطئ بها المطورون بالرغم من سهولة استخدامها، فقد يقع المطورون في أخطاء بسبب هذه التفاصيل الدقيقة في اللغة، مما يستغرق منهم وقتًا طويلًا لإصلاحها. في هذا المقال. سنسلط الضوء في هذا المقال على 10 من الأخطاء الأكثر شيوعًا التي يجب على مطوري PHP توخّي الحذر منها. الخطأ 1: ترك مراجع References المصفوفة معلّقة بعد انتهاء حلقات foreach يمكن أن يكون استخدام المراجع في حلقات foreach مفيدًا إذا أردتَ العمل على كل عنصر من المصفوفة التي تمر عليها، فليكن لدينا مثلًا ما يلي: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8) ولكن إن لم تكن حذرًا، فقد يكون لهذه المراجع بعض الآثار الجانبية والعواقب غير المرغوب فيها، فمثلًا سيبقى المتغير $value ضمن النطاق Scope وسيحتفظ بمرجع إلى العنصر الأخير من المصفوفة بعد تنفيذ الشيفرة البرمجية في المثال السابق، وبالتالي قد تؤدي العمليات اللاحقة المُطبَّقة على المتغير $value إلى تعديل العنصر الأخير في المصفوفة عن غير قصد. تذكّر أن حلقة foreach لا تنشئ نطاقًا، وبالتالي يُعَد المتغير $value في المثال السابق مرجعًا ضمن النطاق العلوي للسكريبت، حيث تضبط حلقة foreach المرجع للإشارة إلى العنصر التالي من المصفوفة $arr في كل تكرار، ويبقى المتغير $value يؤشّر إلى العنصر الأخير من المصفوفة $arr، ويبقى ضمن النطاق بعد اكتمال الحلقة. إليك فيما يلي مثال عن نوع الأخطاء المربكة التي يمكن أن تؤدي إلى هذه المشكلة: $array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // من خلال المرجع echo implode(',', $array), "\n"; foreach ($array as $value) {} // من خلال القيمة (أي النسخة) echo implode(',', $array), "\n"; وسيكون خرج الشيفرة البرمجية السابقة ما يلي: 1,2,3 1,2,3 1,2,2 هذا ليس خطأ مطبعيًا، فالقيمة الأخيرة في السطر الأخير هي 2 وليست 3، والسبب هو بقاء المصفوفة $array دون تغيير بعد المرور على حلقة foreach الأولى، ولكن يُترَك المتغير $value بوصفه مرجعًا مُعلَّقًا للعنصر الأخير في المصفوفة $array لأن حلقة foreach وصلت إلى المتغير $value من خلال المرجع، لذا ستحدث أشياء غريبة عندما نمر على حلقة foreach الثانية، حيث تنسخ حلقة foreach كل عنصر تسلسلي من عناصر المصفوفة $array إلى المتغير $value في كل خطوة من الحلقة بسبب الوصول إلى هذا المتغير من خلال القيمة (أي من خلال النسخة). إليك ما يحدث في كل خطوة من حلقة foreach الثانية: تمرير القيمة 1: يؤدي إلى نسخ العنصر الأول من المصفوفة $array[0] (أي القيمة "1") إلى المتغير $value (وهو مرجع إلى العنصر $array[2])، لذا سيساوي $array[2] الآن القيمة 1، وستحتوي المصفوفة $array على العناصر [1, 2, 1]. تمرير القيمة 2: يؤدي إلى نسخ العنصر الثاني من المصفوفة $array[1] (أي القيمة "2") إلى المتغير $value (وهو مرجع إلى العنصر $array[2])، لذا سيساوي $array[2] الآن القيمة 2، وستحتوي المصفوفة $array على العناصر [1, 2, 2]. تمرير القيمة 3: يؤدي إلى نسخ العنصر الثالث من المصفوفة $array[2] (الذي يساوي القيمة "2" الآن) إلى المتغير $value (وهو مرجع إلى العنصر $array[2])، لذلك لا يزال العنصر $array[2] يساوي القيمة 2، وستحتوي المصفوفة $array الآن على العناصر [1, 2, 2]. يمكن الاستفادة من استخدام المراجع في حلقات foreach دون التعرض لخطر هذه الأنواع من المشاكل من خلال استدعاء الدالة unset() مع المتغير بعد حلقة foreach مباشرةً لإزالة المرجع كما في المثال التالي: $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // لم يَعُد المتغير $value مرجعًا إلى العنصر $arr[3] الخطأ 2: سوء فهم سلوك الدالة isset() تعيد الدالة isset() القيمة false في حالة عدم وجود عنصر، وتعيد أيضًا false للقيم null، حيث يُسبّب هذا السلوك مشكلات، فليكن لدينا المثال التالي: $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // افعل شيئًا هنا عند عدم ضبط 'keyShouldBeSet' } يُفترَض أننا نريد التحقق ممّا إذا كان keyShouldBeSet مضبوطًا في $data، ولكن ستعيد الدالة isset($data['keyShouldBeSet']) أيضًا القيمة false حتى عند ضبط $data['keyShouldBeSet'] على القيمة null، وبالتالي يوجد تناقض مع ما ذكرناه سابقًا. إليك أيضًا المثال التالي: if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; } تفترض الشيفرة البرمجية السابقة أنه إذا أعادت $_POST['active'] القيمة true، فسيكون postData مضبوطًا، وبالتالي ستعيد الدالة isset($postData) القيمة true، وتفترض أيضًا أن الطريقة الوحيدة التي ستعيد بها الدالة isset($postData) القيمة false هي إذا أعادت $_POST['active'] القيمة false، ولكن ستعيد الدالة isset($postData) القيمة false عند ضبط $postData على القيمة null، وبالتالي يمكن أن تعيد الدالة isset($postData) القيمة false حتى إذا أعادت $_POST['active'] القيمة true، لذا يوجد تناقض مع ما ذكرناه سابقًا. إذا كان الهدف من الشيفرة البرمجية السابقة هو التحقق مما إذا كانت $_POST['active'] تعيد القيمة true، فسيكون الاعتماد على الدالة isset() لذلك قرارًا برمجيًا سيئًا على أيّة حال، لذا يُفضَّل إعادة التحقق من $_POST['active'] بدلًا من ذلك كما يلي: if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; } يُعَد استخدام التابع array_key_exists() حلًا أكثر قوة بالنسبة للحالات التي يكون فيها من المهم التحقق مما إذا كان المتغير مضبوطًا فعليًا، أو للتمييز بين متغير غير مضبوط ومتغير مضبوط على القيمة null، فمثلًا يمكننا إعادة كتابة المثال الأول من المثالين السابقين كما يلي: $data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // افعل ذلك عند عدم ضبط 'keyShouldBeSet' } إذا جمعنا بين التابعين array_key_exists() و get_defined_vars()، فيمكننا التحقق بطريقة موثوقة من ضبط متغير ضمن النطاق الحالي كما يلي: if (array_key_exists('varShouldBeSet', get_defined_vars())) { // المتغير $varShouldBeSet موجود في النطاق الحالي } الخطأ 3: الخلط بين الإعادة باستخدام المرجع وإعادتها باستخدام القيمة ليكن لدينا مقطع الشيفرة البرمجية التالي: class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; إذا شغّلتَ الشيفرة البرمجية السابقة، فستحصل على ما يلي: PHP Notice: Undefined index: test in /path/to/my/script.php on line 21 تكمن المشكلة في أن هذه الشيفرة البرمجية تخلط بين إعادة المصفوفات باستخدام المرجع وإعادتها باستخدام القيمة، حيث إن لم تخبر لغةَ PHP صراحةً بإعادة المصفوفة باستخدام المرجع (أي باستخدام &)، فستعيد لغة PHP المصفوفة باستخدام القيمة افتراضيًا، وهذا يمثّل إعادة نسخة من المصفوفة، وبالتالي لن تتمكّن الدالة المُستدعاة والمستدعِي من الوصول إلى النسخة نفسها من المصفوفة. يعيد استدعاء الدالة getValues() السابق نسخة من المصفوفة $values بدلًا من إعادة مرجع إليها. إذًا لنعيد النظر إلى السطرين الرئيسيين من المثال السابق: // تعيد الدالة getValues() نسخة من المصفوفة $values، مما يؤدي إلى إضافة عنصر 'test' // إلى نسخة من المصفوفة $values، وليس إلى المصفوفة $values نفسها $config->getValues()['test'] = 'test'; // تعيد الدالة getValues() مرة أخرى نسخة أخرى من المصفوفة $values، ولا تحتوي هذه النسخة // على عنصر 'test'، ولذلك نحصل على رسالة فهرس غير مُعرَّف "undefined index" echo $config->getValues()['test']; أحد الحلول الممكنة لهذه المشكلة هو حفظ النسخة الأولى من المصفوفة $values التي تعيدها الدالة getValues()، ثم العمل على تلك النسخة لاحقًا كما في المثال التالي: $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test']; ستعمل هذه الشيفرة البرمجية بنجاح، أي أنها ستعطي test دون إنشاء أيّ رسالة فهرس غير مُعرَّف "undefined index"، ولكن قد يكون هذا الأسلوب مناسبًا أو غير مناسب اعتمادًا على ما تحاول تحقيقه، إذ لن تعدّل هذه الشيفرة البرمجية المصفوفة $values الأصلية، لذا إذا أردتَ أن تؤثر تعديلاتك (مثل إضافة عنصر test) على المصفوفة الأصلية، فيجب أن تعدّل الدالة getValues() لإعادة مرجع إلى المصفوفة $values نفسها. يمكن تحقيق ذلك من خلال إضافة & قبل اسم الدالة، مما يشير إلى أنها يجب أن تعيد مرجعًا كما يلي: class Config { private $values = []; // إعادة مرجع إلى المصفوفة $values الفعلية public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; وسيكون الخرج test كما هو متوقع. لنجعل الآن الأمور أكثر إرباكًا كما في مقتطف الشيفرة البرمجية التالي: class Config { private $values; // استخدام كائن مصفوفة ArrayObject بدلًا من المصفوفة public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test']; إذا اعتقدتَ أن هذه الشيفرة البرمجية ستؤدي إلى خطأ "undefined index" نفسه الموجود في مثال المصفوفة array السابق، فأنت مخطئ، إذ ستعمل هذه الشيفرة البرمجية بنجاح، والسبب هو أن لغة PHP تمرّر الكائنات من خلال المرجع دائمًا على عكس المصفوفات، فالكائن ArrayObject هو كائن من مكتبة PHP المعيارية -أو كائن SPL- يحاكي استخدام المصفوفات، ولكنه يعمل بوصفه كائنًا. ليس من الواضح دائمًا في لغة PHP ما إذا كنت تتعامل مع نسخة أو مرجع كما توضح هذه الأمثلة، لذلك يجب أن تفهم هذه السلوكيات الافتراضية مثل تمرير المتغيرات والمصفوفات من خلال القيمة وتمرير الكائنات من خلال المرجع، ويجب التحقق بعناية من توثيق واجهة برمجة التطبيقات API الخاصة بالدالة التي تستدعيها لمعرفة ما إذا كانت تعيد قيمة، أو نسخة من مصفوفة، أو مرجعًا إلى مصفوفة، أو مرجعًا إلى كائن. يجب أيضًا تجنب ممارسة إعادة مرجع إلى مصفوفة أو كائن ArrayObject، لأنه يوفر للمستدعِي القدرة على تعديل البيانات الخاصة للنسخة، وهذا يناقض مفهوم التغليف Encapsulation، لذا يُفضَّل استخدام النمط القديم للجوالب Getters والضوابط Setters كما في المثال التالي: class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue' تمنح هذه الطريقة المستدعِي القدرةَ على ضبط أو جلب أيّ قيمة من المصفوفة دون توفير وصول عام إلى المصفوفة $values الخاصة نفسها. الخطأ 4: إجراء الاستعلامات ضمن حلقة يمكن أن تصادف شيئًا كما يلي إذا كانت شيفرة PHP لا تعمل لديك: $models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); } قد لا يكون هناك أيّ شيء خاطئ، ولكن إذا اتبعت المنطق الموجود في هذه الشيفرة البرمجية، فقد تجد أن الاستدعاء البسيط السابق $valueRepository->findByValue() يؤدي في النهاية إلى استعلامٍ من نوعٍ ما كما يلي: $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue); لذا سيؤدي كل تكرار للحلقة السابقة إلى استعلام منفصل لقاعدة البيانات، فمثلًا إذا زوّدتَ الحلقة بمصفوفةٍ مكونة من 1000 قيمة، فستنشئ 1000 استعلام منفصل للمورد، وإذا اُستدعِي مثل هذا السكربت ضمن خيوط Threads متعددة، فيُحتمَل أن يؤدي ذلك إلى توقف النظام بأكمله، لذا يجب معرفة متى تُجري شيفرتك البرمجية الاستعلامات وجمع القيم ثم تشغيل استعلام واحد لجلب جميع النتائج كلما أمكن ذلك. أحد الأمثلة على الأماكن الشائعة إلى حدٍ ما التي ترى فيها إجراء الاستعلام بطريقة غير فعالة (أي ضمن حلقة) هو عند نشر استمارة مع قائمة من القيم (المعرّفات مثلًا)، ثم ستمر الشيفرة البرمجية ضمن حلقة على المصفوفة وتجري استعلام SQL منفصل لكل معرّف لاسترداد بيانات السجل الكاملة لكل معرّف من هذه المعرّفات كما يلي: $data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); } ولكن يمكن تحقيق الشيء نفسه بفعالية أكبر في استعلام SQL واحد كما يلي: $data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } } لذلك يجب أن تعرف متى تجري شيفرتك البرمجية الاستعلامات سواءً بطريقة مباشرة أو غير مباشرة، ويجب أن تجمع القيم ثم تشغّل استعلامًا واحدًا لجلب جميع النتائج كلما أمكنك ذلك، ولكن يجب توخي الحذر لكي لا نحصل على خطأ PHP الشائع التالي. الخطأ 5: تزييف استخدام الذاكرة وعدم كفاءتها يكون جلب العديد من السجلات في وقت واحد أكثر كفاءة من تشغيل استعلام واحد لجلب كل صف، ولكن يمكن أن يؤدي هذا النهج إلى حالة "نفاد الذاكرة Out of Memory" في مكتبة libmysqlclient عند استخدام إضافة PHP التي هي mysql. ليكن لدينا مثلًا مربع اختبار مع موارد محدودة (512 ميجابايت من ذاكرة RAM) وقاعدة بيانات MySQL وواجهة سطر الأوامر php-cli، حيث سنهيّئ جدول قاعدة بيانات كما يلي: // الاتصال بقاعدة بيانات mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // إنشاء جدول مؤلّف من 400 عمود $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // كتابة 2 مليون صف for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); } لنتحقّق الآن من استخدام الموارد كما يلي: // الاتصال بقاعدة بيانات mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n"; ويكون الخرج كما يلي: Before: 224704 Limit 1: 224704 Limit 10000: 224704 لاحظ أن الاستعلامات مُدارة داخليًا بأمان فيما يتعلق بالموارد، ولكن يمكن التأكد من ذلك من خلال زيادة الحد الأقصى مرة أخرى، حيث سنضبطه على القيمة 100000، وسينتج ما يلي: PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11 المشكلة هنا هي الطريقة التي تعمل بها وحدة mysql الخاصة بلغة PHP، فهي مجرد وكيل لمكتبة libmysqlclient، والتي تذهب إلى الذاكرة مباشرةً عند تحديد جزء من البيانات. لا يدير مدير PHP الذاكرة، وبالتالي لن تعرض الدالة memory_get_peak_usage() أيّ زيادة في استخدام الموارد عندما نزيد الحد الأقصى في استعلامنا. يؤدي ذلك إلى ظهور مشاكل مثل المشكلة السابقة، إذ سنُخدَع من خلال الاعتقاد بأن إدارة ذاكرتنا جيدة، ولكنها سيئة فعليًا وسنواجه مثل هذه المشكلة. يمكنك تجنب هذا التزييف من خلال استخدام الوحدة mysqlnd بدلًا من ذلك، بالرغم من أنها لن تؤدي إلى تحسين استخدام الذاكرة، حيث تُصرَّف Compiled هذه الوحدة بوصفها إضافةً أصيلة Native للغة PHP وتستخدم مدير ذاكرة PHP، لذلك إذا أجرينا الاختبار السابق باستخدام الوحدة mysqlnd بدلًا من الوحدة mysql، فسنحصل على صورة أكثر واقعية لاستخدام ذاكرتنا. Before: 232048 Limit 1: 324952 Limit 10000: 32572912 تستخدم الوحدة mysql ضعف عدد الموارد التي تستخدمها الوحدة mysqlnd لتخزين البيانات وفقًا لتوثيق PHP، لذا يستخدم السكربت الأصلي الذي يستخدم وحدة mysql ذاكرةً أكبر مما هو موضّح هنا فعليًا (حوالي ضعف ذلك). يمكن تجنب مثل هذه المشاكل من خلال تحديد حجم استعلاماتك واستخدام حلقة ذات عدد صغير من التكرارات كما يلي: $totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); } إذا أمعنا النظر في هذا الخطأ والخطأ رقم 4، فإننا سندرك أن هناك توازنًا صحيًا تحتاج شيفرتك البرمجية إلى تحقيقه بطريقة مثالية، حيث يجب إما أن تكون استعلاماتك صغيرة ومتكررة جدًا من ناحية، أو أن يكون كل استعلام فردي كبيرًا جدًا من ناحية أخرى، لذا يجب إيجاد توازن بينهما، إذ يمكن أن يسبّب استخدام أيٍّ منهما إلى الحد الأقصى مشكلات مع شيفرة PHP البرمجية التي لن تعمل بطريقة صحيحة. الخطأ 6: تجاهل مشكلات الترميز Unicode/UTF-8 تُعَد هذه المشكلة خاصة بلغة PHP بحد ذاتها أكثر من كونها مشكلة قد تظهر أثناء تنقيح أخطاء شيفرة PHP، ولكن هذه المشكلة لم تُعالَج بطريقة كافية، إذ كان من المقرر جعل نواة PHP 6 متوافقةً مع الترميز الموحّد Unicode، وجرى تعليق ذلك عند تعليق تطوير PHP 6 في عام 2010، ولكن ذلك لا يعفي المطور بأيّ حال من الأحوال من تقديم ترميز UTF-8 بطريقة صحيحة وتجنب الافتراض الخاطئ بأن جميع السلاسل النصية ستكون "نصًا عاديًا قديمًا يستخدم معيار ASCII". تشتهر الشيفرة البرمجية التي تفشل في معالجة السلاسل النصية التي ليست ASCII بإدخال أخطاء هايزنبغ Heisenbug المعقدة في شيفرتك البرمجية، إذ قد تسبّب حتى استدعاءات strlen($_POST['name']) البسيطة مشكلات إذا حاول شخص يحمل اسم عائلة مثل "Schrödinger" التسجيل في نظامك. إليك فيما يلي قائمة صغيرة لتجنب مثل هذه المشكلات في شيفرتك البرمجية: إذا كنت لا تعرف الكثير عن ترميز Unicode و UTF-8، فيجب أن تتعلّم الأساسيات على الأقل. تأكد من استخدام دوال mb_* دائمًا بدلًا من دوال السلاسل النصية القديمة، وتأكد من تضمين إضافة "تعدد البايتات multibyte" في إصدار PHP الخاص بك. تأكد من ضبط قاعدة البيانات والجداول الخاصة بك لاستخدام ترميز Unicode، إذ لا تزال العديد من إصدارات MySQL تستخدم معيار latin1 افتراضيًا. تذكّر أن الدالة json_encode() تحوّل الرموز التي ليست رموز ASCII، حيث تحوّل السلسلة النصية "Schrödinger" إلى "Schr\u00f6dinger" مثلًا، ولكن التابع serialize() لا يفعل ذلك. تأكّد من أن ملفات شيفرة PHP البرمجية الخاصة بك مشفَّرة أيضًا باستخدام ترميز UTF-8 لتجنب التعارض عند ضم السلاسل النصية مع ثوابت السلاسل النصية المكتوبة أو المضبوطة. اطّلع على مقال معالجة الملفات والبيانات المرمزة بترميز UTF-8 في PHP لمزيدٍ من المعلومات. الخطأ 7: افتراض أن المصفوفة $_POST ستحتوي على بيانات POST الخاصة بك دائمًا لن تحتوي المصفوفة $_POST على بيانات POST الخاصة بك دائمًا على عكس ما يدل عليه اسمها، إذ يمكن أن تكون فارغة. لنفترض مثلًا أننا أنشأنا طلب خادم باستخدام استدعاء التابع jQuery.ajax() كما يلي: // js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' }); لاحظ نوع المحتوى contentType: 'application/json' هنا، حيث نرسل البيانات بتنسيق JSON، وهو أمر شائع جدًا لواجهات برمجة التطبيقات، وهو الإعداد الافتراضي للنشر في خدمة AngularJS التي هي $http مثلًا. نفرّغ معلومات محتوى المصفوفة $_POST من طرف الخادم في مثالنا كما يلي: // php var_dump($_POST); والمفاجأة أن النتيجة ستكون كما يلي: array(0) { } لم تظهر سلسلة JSON النصية الخاصة بنا {a: 'a', b: 'b'}، حيث تحلّل لغة PHP حِمل طلب POST تلقائيًا عندما يكون نوع المحتوى application/x-www-form-urlencoded أو multipart/form-data فقط، إذ كان هذان النوعان من المحتوى هما النوعان الوحيدان المُستخدَمان منذ سنوات عند تنفيذ المصفوفة $_POST الخاصة بلغة PHP، لذا لا تحمّل لغة PHP حِمل طلب POST تلقائيًا مع أيّ نوع محتوى آخر حتى الأنواع التي تحظى بشعبية كبيرة اليوم مثل application/json. تُعَد المصفوفة $_POST ذات نطاق عام عالي Superglobal، لذا إذا عدّلناها مرة واحدة (ويُفضَّل أن يكون ذلك في وقت مبكر من السكربت)، فستكون القيمة المُعدَّلة (بما في ذلك حِمل طلب POST) قابلةً للإشارة إليها من شيفرتنا البرمجية. يُعَد ذلك أمرًا مهمًا، لأن أطر عمل PHP وجميع السكربتات المخصَّصة تقريبًا تستخدم المصفوفة $_POST استخدامًا شائعًا لاستخراج بيانات الطلب وتحويلها. نحتاج إلى تحليل محتويات الطلب يدويًا (أي فك تشفير بيانات JSON) وتعديل المتغير $_POST عند معالجة حِمل طلب POST مع نوع المحتوى application/json مثلًا كما يلي: // php $_POST = json_decode(file_get_contents('php://input'), true); إذا فرّغنا معلومات محتوى المصفوفة $_POST بعد ذلك، فسنرى أنها تتضمّن حِمل طلب POST الصحيح كما في المثال التالي: array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" } الخطأ الشائع 8: الاعتقاد بأن لغة PHP تدعم نوع بيانات المحارف Character اطّلع على الشيفرة البرمجية التالية وحاول تخمين ما سينتج: for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; } إذا أجبت أنها ستطبع المحارف من 'a' إلى 'z'، فقد تتفاجأ عندما تعرف أنك مخطئ، حيث أنها ستطبع المحارف من 'a' إلى 'z'، ولكنها ستطبع أيضًا 'aa' إلى 'yz'، إذ لا يوجد نوع البيانات char في لغة PHP، بينما يوجد نوع بيانات السلاسل النصية string فقط، وبالتالي سينتج عن زيادة السلسلة النصية z بمقدار واحد في لغة PHP السلسلة النصية aa: php> $c = 'z'; echo ++$c . "\n"; aa ولكن تُعَد السلسلة النصية aa أصغر من z من معجميًا: php> var_export((boolean)('aa' < 'z')) . "\n"; true لذلك تطبع الشيفرة البرمجية الحروف من a إلى z، ولكنها تطبع أيضًا بعد ذلك من aa إلى yz، وتتوقف عندما تصل إلى za، وهي القيمة الأولى الأكبر من z التي تصادفها: php> var_export((boolean)('za' < 'z')) . "\n"; false لذا استخدم الطريقة التالية للمرور ضمن حلقة على القيم من 'a' إلى 'z' في لغة PHP بصورة صحيحة: for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; } أو استخدم الطريقة التالية: $letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; } الخطأ 9: تجاهل قواعد ومعايير كتابة الشيفرة البرمجية لا يؤدي تجاهل قواعد كتابة الشيفرة البرمجية مباشرةً إلى الحاجة إلى تنقيح أخطاء شيفرة PHP، ولكنه لا يزال أحد أهم الأشياء التي يجب مناقشتها، إذ قد يؤدي تجاهل هذه المعايير إلى حدوث عدد كبير من المشكلات في المشروع مثل الحصول على شيفرة برمجية غير متناسقة في أحسن الأحوال لأن كل مطور ينجز عمله بطريقته الخاصة، أو الحصول على شيفرة PHP لا تعمل أو من الصعب -أو من المستحيل في بعض الأحيان- التنقل ضمنها، مما يصعّب تنقيح أخطائها وتحسينها وصيانتها في أسوأ الأحوال. يؤدي ذلك إلى تخفيض إنتاجية فريقك بما في ذلك الكثير من الجهد الضائع أو غير الضروري. توجد توصية لمعايير PHP أو PHP Standards Recommendation -أو PSR اختصارًا، والتي تتألف من المعايير الخمسة التالية: PSR-0: معيار التحميل التلقائي. PSR-1: معيار كتابة الشيفرة البرمجية الأساسي. PSR-2: دليل تنسيق كتابة الشيفرة البرمجية. PSR-3: واجهة المسجِّل. PSR-4: المحمِّل التلقائي. لقد أُنشِئت معايير PSR بناءً على مدخلات من المشرفين على المنصات الأشهر في السوق لتوفر إرشادات وممارسات موحدة لتنظيم كتابة الأكواد في PHP. وقد وُضعت هذه المعايير من قبل مجموعة من المطورين والمشرفين على أبرز المنصات وأطر العمل الشهيرة مثل Zend و Drupal و Symfony و Joomla وغيرها في هذه المعايير، وتتبع هذه المنصات تلك المعايير، ويشارك الآن إطار عمل PEAR الذي حاول أن يكون معيارًا لسنوات قبل ذلك في وضع معايير PSR. لا يهم تقريبًا ما هو معيار كتابة الشيفرة البرمجية الخاص بك طالما أنك توافق على المعيار وتلتزم به، ولكن يُعَد اتباع معايير PSR فكرة جيدة إن لم يكن لديك سبب مقنع في مشروعك يخالفها، إذ أصبحت هناك العديد من الفرق والمشاريع التي تتوافق مع معايير PSR التي يُعِدّها أغلب مطوري PHP بوصفها المعيار الحالي، لذا سيساعد استخدامها في ضمان معرفة المطورين الجدد بمعيار كتابة الشيفرة البرمجية الخاص بك وسيشعرون بالارتياح عند انضمامهم إلى فريقك. الخطأ 10: استخدام الدالة empty() استخدامًا خاطئًا يفضِّل بعض مطوري PHP استخدام الدالة empty() لإجراء عمليات فحص منطقية لكل شيء تقريبًا، ولكن توجد حالات يمكن أن يؤدي فيها ذلك إلى الارتباك. أولًا، إذا عُدنا إلى المصفوفات ونُسخ كائن المصفوفة ArrayObject (الذي يحاكي المصفوفات)، فمن السهل افتراض أن المصفوفات ونسخ الكائن ArrayObject ستتصرف بطريقة مماثلة بسبب تشابههما، ولكنه افتراض خطير كما هو موضّح فيما يلي في الإصدار PHP 5.0: // PHP 5.0 أو بعد: $array = []; var_dump(empty($array)); // خرجها bool(true) $array = new ArrayObject(); var_dump(empty($array)); // خرجها bool(false) // لماذا لا ينتج كلاهما الخرج نفسه؟ وستكون النتائج مختلفة في الإصدارات التي قبل الإصدار PHP 5.0: // الإصدارات التي قبل الإصدار PHP 5.0: $array = []; var_dump(empty($array)); // خرجها bool(false) $array = new ArrayObject(); var_dump(empty($array)); // خرجها bool(false) تحظى هذه الطريقة بشعبية كبيرة لسوء الحظ، فمثلًا هذه هي الطريقة التي يعيد بها كائن Zend\Db\TableGateway في إطار عمل Zend Framework 2 البيانات عند استدعاء الدالة current() مع نتيجة الدالة TableGateway::select()، ويمكن للمطور أن يصبح بسهولة ضحيةً لهذا الخطأ مع مثل هذه البيانات. يمكن تجنب هذه المشكلات من خلال التحقق من وجود بنى مصفوفات فارغة باستخدام الدالة count() كما يلي: // لاحظ أن ما يلي يعمل في جميع إصدارات PHP (سواء قبل أو بعد الإصدار 5.0): $array = []; var_dump(count($array)); // خرجها int(0) $array = new ArrayObject(); var_dump(count($array)); // خرجها int(0) تحوّل لغة PHP القيمة 0 إلى false، فيمكن أيضًا استخدام الدالة count() ضمن شروط تعليمة if للتحقق من المصفوفات الفارغة. تجدر الإشارة أيضًا إلى أن الدالة count() في PHP لها تعقيد ثابت Constant Complexity (أو O(1)) على المصفوفات، مما يوضّح أنه الاختيار الصحيح. يوجد مثال آخر لذلك عندما تمثّل الدالة empty() خطرًا من خلال دمجها مع دالة الأصناف السحرية __get(). لنعرّف مثلًا صنفين Class مع وجود الخاصية test في كليهما، حيث نعرّف أولًا الصنف Regular الذي يتضمن الخاصية العادية test كما يلي: class Regular { public $test = 'value'; } نعرّف بعد ذلك الصنف Magic الذي يستخدم التابع السحري __get() للوصول إلى الخاصية test الخاصة به كما يلي: class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } } لنرى الآن ما سيحدث عندما نحاول الوصول إلى الخاصية test لكل صنف كما يلي: $regular = new Regular(); var_dump($regular->test); // خرجها string(4) "value" $magic = new Magic(); var_dump($magic->test); // خرجها string(4) "value" يبدو كل شيء على ما يرام حتى الآن، ولكن لنرى الآن ما يحدث عندما نستدعي الدالة empty() مع كل منهما كما يلي: var_dump(empty($regular->test)); // خرجها bool(false) var_dump(empty($magic->test)); // خرجها bool(true) إذا اعتمدنا على الدالة empty()، فيمكن تضليلنا للاعتقاد بأن الخاصية test الخاصة بالصنف $magic فارغة، بينما هي مضبوطة على القيمة 'value'. لسوء الحظ، إذا استخدم الصنفُ التابع السحري __get() لاسترداد قيمة خاصيةٍ ما، فلا توجد طريقة مضمونة للتحقق مما إذا كانت قيمة الخاصية فارغة أم لا. يمكنك خارج نطاق الصنف التحقق فقط من إعادة القيمة null، وهذا لا يعني بالضرورة عدم ضبط المفتاح المقابل، لأنه كان من الممكن ضبطه على القيمة null. إذا حاولنا الإشارة إلى خاصية غير موجودة لنسخة من الصنف Regular، فستظهر ملاحظة مشابهة لما يلي: Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0 لاحظ أنه يجب استخدام التابع empty() بحذر لأنه قد يؤدي إلى نتائج مربكة ومضللة. الخلاصة يمكن أن تؤدي سهولة استخدام لغة PHP إلى جعل المطورين يشعرون بالراحة الزائدة في التعامل معها، مما قد يؤدي إلى تجاهل بعض الأخطاء الدقيقة التي تحتاج إلى تنقيح ومعالجة وعدم مراعاة بعض الفروق الدقيقة والخصوصيات في اللغة، ويمكن أن يؤدي ذلك إلى عدم عمل شيفرة PHP بنجاح وظهور إحدى المشكلات التي تعرضنا لها في هذا المقال. لقد تطورت لغة PHP تطورًا ملحوظًا على مدار تاريخها الطويل وشهدت تحسينات ومميزات عديدة، لذا يُعَد التعرف على تفاصيلها الدقيقة أمرًا جديرًا بالاهتمام، لأنها ستساعد على ضمان أن البرنامج الذي تنتجه أكثر قابلية للتوسع والقوة والصيانة. ترجمة -وبتصرُّف- للمقال Buggy PHP Code: The 10 Most Common Mistakes PHP Developers Make لصاحبه Ilya Sanosian. اقرأ أيضًا تعرف على لغة PHP تعلم لغة PHP اصطلاحات ومواضيع متفرقة مهمة لكل مبرمج PHP التعامل مع الأخطاء في PHP الأخطاء العشرة الأكثر شيوعًا في شيفرة بايثون Python البرمجية
-
سنتحدث في هذا المقال عن معمارية MVC الخاصة بإطار عمل لارافيل Laravel، فبنية MVC هي مبدأ تصميم الويب الذي يتكون من النموذج Model المسؤول عن التواصل مع قاعدة البيانات، والمتحكِّم Controller وهو المكان الذي نخزّن فيه شيفرة تطبيقنا البرمجية، والعرض View وهو الواجهة الأمامية للتطبيق. يوجد مُوجّه Router في تطبيق الويب عادةً، وهذا ما ناقشناه في المقال السابق، حيث يؤشّر الموجّه إلى المتحكم، ويبحث المتحكم عن البيانات المطلوبة في قاعدة البيانات عبر النموذج، ثم يضع البيانات المُسترَّدة إلى الموقع المقابل في العرض، وأخيرًا يعيد هذا العرض إلى المستخدم. طبقة العرض لنبدأ أولًا بطبقة بالعرض، حيث عرّفنا في المقال السابق الوِجهة Route التالية: Route::get('/', function () { return view('welcome'); }); تؤشّر هذه الوِجهة إلى العرض welcome المُخزَّن في المجلد resources/views وله الامتداد .blade.php، حيث يُعلِم هذا الامتداد لارافيل أننا نستخدم نظام قوالب Blade الذي سنتحدث عنه لاحقًا. يمكننا إنشاء عرض آخر من خلال إنشاء ملف آخر له الامتداد نفسه في المجلد views، إذًا لننشئ العرض greetings.blade.php التالي: <html> <body> <h1>Hello!</h1> </body> </html> تحدثنا أيضًا في المقال السابق عن إمكانية تمرير البيانات من الوِجهة إلى العرض كما يلي: Route::get('/', function () { return view('greeting', ['name' => 'James']); }); أسندنا في المثال السابق القيمة 'James' إلى المتغير name، ومرّرنا هذا المتغير إلى العرض greeting.blade.php الذي أنشأناه مسبقًا، ويمكننا استخدام الأقواس المزدوجة المتعرجة {{ }} لعرض هذه البيانات في العرض greetings.blade.php كما يلي: <html> <body> <h1>Hello, {{ $name }}</h1> </body> </html> من الشائع تنظيم ملفات العرض في مجلدات مختلفة بالنسبة لتطبيقات الويب الكبيرة، فمثلًا يمكن أن يحتوي تطبيقك على لوحة إدارة، وبالتالي ستخزّن العروض Views المقابلة لها في مجلد فرعي اسمه admin مثلًا، وتذكّر استخدام . بدلًا من / عندما تؤشّر إلى ملفات العرض في بنية متداخلة من الموجّه، حيث يؤشّر المثال التالي إلى الملف home.blade.php الموجود في المجلد views/admin/: Route::get('/admin', function () { return view('admin.home'); }); صيغة قوالب Blade تستخدم عروض لارافيل قالب Blade افتراضيًا، وهو مشابه لنظام المكونات في إطار عمل Vue.js. تُعَد العروض مستندات HTML، ويضيف قالب Blade ميزات برمجية إليها مثل نظام الوراثة والتحكم في التدفق والحلقات والمفاهيم البرمجية الأخرى، مما يؤدي إلى إنشاء صفحة ويب أكثر ديناميكية مع كتابة تعليمات برمجية أقل. التعليمة الشرطية if يمكننا إنشاء تعليمات if باستخدام التوجيهات Directives التي هي @if و @elseif و @else و @endif، حيث تعمل هذه التعليمات مثل تعليمات if التي نراها في لغات البرمجة الأخرى. @if ($num == 1) <p>The number is one.</p> @elseif ($num == 2) <p>The number is two.</p> @else <p>The number is three.</p> @endif يمكننا اختبار هذه الشيفرة البرمجية باستخدام الوِجهة كما يلي: Route::get('/number/{num}', function ($num) { return view('view', ['num' => $num]); }); نفترض هنا أن المتغير num يمكن أن يكون قيمته 1 أو 2 أو 3 فقط. التعليمة switch تكون بنية هذه التعليمة كما يلي: @switch($i) @case(1) First case. . . @break @case(2) Second case. . . @break @default Default case. . . @endswitch الحلقات يمكن أن نمثل الحلقات بحلقة for بسيطة: @for ($i = 0; $i < 10; $i++) The current value is {{ $i }} @endfor أو بحلقة foreach، حيث يساوي المتغير $user العنصر التالي من $users لكل تكرار في المثال التالي: @foreach ($users as $user) <p>This is user {{ $user->id }}</p> @endforeach يمكن أن نمثّل الحلقات بحلقة while أيضًا: @while (true) <p>I'm looping forever.</p> @endwhile تُعَد هذه التعليمات بسيطة جدًا، وتعمل تمامًا مثل نظيراتها في لغة PHP، ولكن يوجد شيء خاص بهذه الحلقات هو المتغير $loop، فمثلًا ليكن لدينا شيء نريد عرضه مرة واحدة فقط، حيث يمكنك تطبيق ما يلي في التكرار الأول: @foreach ($users as $user) @if ($loop->first) <p>This is the first iteration.</p> @endif <p>This is user {{ $user->id }}</p> @endforeach سيُعرَض عنصر الفقرة <p>This is the first iteration.</p> مرة واحدة فقط في التكرار الأول، وتوجد العديد من الخاصيات الأخرى التي يمكنك الوصول إليها، لذا اطلع عليها في توثيق لارافيل الرسمي. لاحظ أنه لا يمكن الوصول إلى المتغير $loop إلا ضمن الحلقة. الصنف الشرطي Conditional Class يُعَد الصنف Class الطريقة الأكثر استخدامًا لإسناد التنسيقات Styles إلى عناصر HTML المختلفة، ويمكننا بسهولة تغيير مظهر هذا العنصر من خلال تغيير صنفه، حيث يقدّم لارافيل طريقة لإسناد الأصناف ديناميكيًا بالاعتماد على المتغيرات كما يلي: <span @class([ 'p-4', 'font-bold' => $isActive, 'bg-red' => $hasError, ])></span> يعتمد الصنفان font-bold و bg-red في المثال السابق على قيمة المتغيرين $isActive و $hasError. بناء تخطيط الصفحة Layout توجد دائمًا بعض أجزاء الصفحة التي ستظهر في صفحات متعددة عندما نبني تطبيق ويب، حيث تُعَد كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا مضيعة للوقت والموارد، ولا يُعَد ذلك جيدًا للصيانة، لذا يجب فصل الجزء الذي سيظهر عدة مرات ووضعه في ملف آخر، وسنستورده ببساطة عندما نحتاج إليه. يقدم إطار عمل لارافيل طريقتين مختلفتين لذلك هما: وراثة القوالب ووراثة المكونات، حيث يُعَد فهم وراثة القوالب أسهل بكثير للمبتدئين، ولكن توفّر المكونات مزيدًا من الميزات، إذًا لنبدأ أولًا بوراثة القوالب. وراثة القوالب نحاول في المثال التالي تعريف صفحة رئيسية، وتكون الوِجهة كما يلي: Route::get('/', function () { return view('home'); }); يستورد العرض layout.blade.php ملفات CSS وجافاسكريبت JavaScript الضرورية، بالإضافة إلى شريط التنقل وتذييل الصفحة التي ستظهر في جميع الصفحات، وسنرث layout.blade.php في الملف home.blade.php. لنضع أولًا ما يلي في الملف layout.blade.php: <html> <head> @yield('title') </head> <body> <div class="container">@yield('content')</div> </body> </html> ولنضع الآن ما يلي في الملف home.blade.php: @extends('layout') @section('title') <title>Home Page</title> @endsection @section('content') <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. . .</p> @endsection لاحظ أن @section('title') يقابل @yield('title')، وأن @section('content') يقابل @yield('content'). سيرى لارافيل أولًا التوجيه @extends('layout') عند عرض الصفحة الرئيسية في مثالنا، وسيعرف أنه يجب عليه الانتقال إلى الملف layout.blade.php. سيحدد لارافيل بعد ذلك موقع @yield('title') ويضع القسم title مكانه، ثم يبحث عن التوجيه @yield('content') ويضع القسم content مكانه. سنحتاج في بعض الأحيان إلى تحميل عرض آخر من العرض الحالي، فمثلًا إذا أردنا إضافة شريط جانبي إلى صفحتنا الرئيسية، وهو شيء سنضمّنه في بعض الصفحات دون صفحات أخرى، فلن نضعه في الملف layout.blade.php، ولكن سيكون من الصعب جدًا صيانته إذا أنشأتَ شريطًا جانبيًا لكل صفحة تتطلب ذلك. يمكننا في مثالنا إنشاء الملف sidebar.blade.php، ثم استخدام التوجيه @include لاستيراد هذا الملف في الصفحة الرئيسية كما يلي: @extends('layout') @section('title') <title>Home Page</title> @endsection @section('content') <p>. . .</p> @include('sidebar') @endsection المكونات Components يشبه نظام المكونات في لارافيل إلى حد كبير النظام الموجود في إطار عمل Vue.js. سنبني صفحة رئيسية مع تخطيطها، ولكننا سنعرّف التخطيط بوصفه مكونًا، لذا سننشئ المجلد components ضمن المجلد resources/views/، ثم ننشئ الملف layout.blade.php بداخله. لنضع أولًا ما يلي في الملف views/components/layout.blade.php: <html> <head> . . . </head> <body> <div class="container">{{ $slot }}</div> </body> </html> يمكن استخدام هذا المكون من خلال استخدام الوسم <x-layout>، وتذكّر دائمًا البادئة x-. <x-layout> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit semper ex varius vehicula. </p> </x-layout> سيُسنَد المحتوى الموجود ضمن العنصر <x-layout> إلى المتغير $slot تلقائيًا، ولكن إذا كان لدينا فتحات Slots متعددة كما في مثالنا الذي يحتوي على قسم title وقسم content، فيمكن حل هذه المشكلة من خلال تعريف عنصر <x-slot> كما يلي: <x-layout> <x-slot name="title"> <title>Home Page</title> </x-slot> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit semper ex varius vehicula. </p> </x-layout> سيُسنَد محتوى <x-slot name="title"> إلى المتغير $title وسيُسنَد المحتوى المتبقي إلى المتغير $slot كما يلي: <html> <head> {{ $title }} </head> <body> <div class="container">{{ $slot }}</div> </body> </html> التعامل مع قواعد البيانات في لارافيل سنناقش أولًا كيفية التعامل مع قواعد البيانات في لارافيل قبل أن نتحدث بمزيد من التفصيل عن طبقة النموذج والمتحكم في بنية MVC. يمكن أن نستخدم ملف .txt بسيط لتخزين المعلومات عند استخدام جافاسكريبت JavaScript و Node.js، ولكن لن ينجح ذلك في تطبيق الويب الواقعي، لذا سنحتاج لاستخدام قاعدة بيانات لتخزين ومعالجتها المعلومات بكفاءة. استخدمنا في مقالنا الحالي أداة Sail الخاصة بلارافيل لإنشاء هذا المشروع، لذا أعدت هذه الأداة قاعدة بيانات MySQL، إذ يجب أن توجد متغيرات البيئة التالية في ملف .env: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=curl_demo DB_USERNAME=sail DB_PASSWORD=password عمليات تهجير Migrations قاعدة البيانات تكون قاعدة البيانات فارغة حاليًا، لذا سنضيف بعض جداول قاعدة البيانات لاستخدامها، حيث ستحتوي هذه الجداول على أعمدة، وستكون للأعمدة أسماء وبعض المتطلبات الخاصة، وتسمى عملية الإعداد هذه بتشغيل عمليات التهجير. تُخزَّن جميع ملفات التهجير في لارافيل ضمن المجلد database/migrations، ويمكننا إنشاء ملف تهجير جديد من خلال استخدام الأمر البسيط التالي، ولكن إذا استخدمتَ أداة Sail لإنشاء مشروع لارافيل، فضع ./vendor/bin/sail بدلًا من php لتشغيل PHP داخل حاوية دوكر Docker، وإن لم تستخدم أداة Sail، فتابع كالمعتاد: php artisan make:migration create_flights_table يمكن تطبيق ملف التهجير كما يلي: php artisan migrate وإذا أردتَ التراجع عن عملية التهجير السابقة، فاستخدم الأمر التالي: php artisan migrate:rollback أو إذا أردتَ إعادة ضبط عمليات التهجير بالكامل والتراجع عن جميع التغييرات المطبقة على قاعدة البيانات، فاستخدم الأمر التالي: php artisan migrate:reset إنشاء جدول لنلقِ نظرة على المثال التالي قبل إنشاء ملف التهجير. افتح ملف التهجير database/migrations/2014_10_12_000000_create_users_table.php التالي الذي يأتي مع لارافيل: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * تشغيل عمليات التهجير */ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('users'); } }; يحتوي هذا الملف على صنف class له تابعان مختلفان، حيث يُستخدَم التابع up() لإنشاء جداول وأعمدة جديدة، بينما يمكن استخدام التابع down() لعكس العمليات التي يطبّقها التابع up(). يستخدم لارافيل داخل التابع up() التابع create() ضمن الصنف Schema لإنشاء جدول جديد بالاسم 'users'. وينشئ كل سطر من السطور 15 إلى 21 في الشيفرة البرمجية السابقة عمودًا مختلفًا مع اسم ونوع مختلفين، فمثلًا ينشئ السطر $table->string('name'); عمودًا بالاسم name ضمن الجدول users، ويجب أن يخزّن هذا العمود سلاسلًا نصية. اطّلع على القائمة الكاملة لأنواع الأعمدة المتوفرة في لارافيل. لن نستعرض جميع هذه الأنواع، بل سنناقش أهمها وسبب اختيارنا لنوع العمود عندما نواجه مشكلات محددة مستقبلًا. لاحظ السطر 17 في الشيفرة البرمجية السابقة $table->string('email')->unique();، حيث يوجد التابع unique() بعد التصريح عن اسم العمود ونوعه، حيث يُسمَّى هذا التابع بمُعدِّل العمود Column Modifier، لأنه يضيف قيودًا إضافية إلى هذا العمود، ويتأكد في هذه الحالة من أن العمود لا يمكن أن يحتوي على قيم متكررة، ويجب أن يكون كل بريد إلكتروني يقدّمه المستخدم فريدًا. اطّلع على القائمة الكاملة لمُعدِّلي الأعمدة المتوفرة في لارافيل. إجراء تغييرات على الجدول يمكن أيضًا تحديث الجداول باستخدام التابع table() كما يلي، حيث تضيف الشيفرة البرمجية التالية عمودًا جديدًا هو العمود age إلى الجدول users الموجود مسبقًا: Schema::table('users', function (Blueprint $table) { $table->integer('age'); }); كما يمكننا إعادة تسمية الجدول باستخدام التابع rename(): Schema::rename($from, $to); ويمكننا حذف جدول نهائيًا كما يلي: Schema::drop('users'); Schema::dropIfExists('users'); البذر أو توليد البيانات Seeding يُستخدَم مولّد البيانات Seeder لإنشاء بيانات وهمية لقاعدة بياناتك، وبالتالي يسهّل عليك إجراء الاختبارات، حيث يمكنك إنشاء مولّد بيانات من خلال تشغيل الأمر التالي: php artisan make:seeder UserSeeder يولّد الأمر السابق مولّد بيانات للجدول users، وسيُوضَع في المجلد database/seeders، حيث وضع ما يلي في الملف UserSeeder.php: <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; class UserSeeder extends Seeder { /** * تشغيل مولّدات بيانات قاعدة البيانات */ public function run(): void { DB::table('users')->insert([ 'name' => Str::random(10), 'email' => Str::random(10).'@gmail.com', 'password' => Hash::make('password'), ]); } } شغّل الأمر التالي لتنفيذ مولّد البيانات السابق وملء جداول قاعدة البيانات بالبيانات الافتراضية: php artisan db:seed UserSeeder باني الاستعلامات Query Builder باني الاستعلامات هو واجهة تسمح لنا بالتفاعل مع قاعدة البيانات، فمثلًا يمكننا استخدام التابع table() الذي يوفره الصنف DB لاسترداد جميع الصفوف الموجودة في الجدول كما يلي: use Illuminate\Support\Facades\DB; $users = DB::table('users')->get(); يمكننا إضافة مزيدٍ من القيود من خلال إضافة التابع where() إلى سلسلة التوابع كما يلي، حيث ستحصل الشيفرة البرمجية التالية على جميع الصفوف التي تكون فيها القيمة المخزنة في العمود age أكبر من أو تساوي 21: $users = DB::table('users')->where('age', '>=', 21)->get(); توجد الكثير من التوابع الأخرى إلى جانب التابعين where() و get()، والتي سنتحدث عنها لاحقًا. طبقة النموذج Model يُعَد النموذج مفهومًا مهمًا جدًا في تصميم الويب الحديث، فهو مسؤول عن التفاعل مع قاعدة بيانات تطبيق الويب، وهو جزء من نظام رابط الكائنات بالعلاقات Object-Relational Mapper -أو ORM اختصارًا- في لارافيل الذي هو نظام Eloquent، حيث يمكن عَدّه باني استعلام مع بعض الميزات الإضافية. يمكننا استخدام الأمر make:model لإنشاء نموذج جديد كما يلي: php artisan make:model Post إذا أردتَ توليد ملف تهجير مقابل للنموذج، فاستخدم الخيار --migration: php artisan make:model Post --migration وسيتضمّن ملف النموذج app/Models/Post.php ما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; . . . } يفترِض النموذج السابق وجود الجدول posts في قاعدة البيانات افتراضيًا، وإذا أردتَ تغيير هذا الإعداد، فاضبط الخاصية $table كما يلي: class Post extends Model { /** * الجدول المرتبط بالنموذج * * @var string */ protected $table = 'my_posts'; } استرداد النموذج يُعَد نموذج Eloquent بانيَ استعلامات قويًا ويسمح بالتواصل مع قاعدة البيانات، فمثلًا يمكننا استخدام التابع all() لاسترداد كافة السجلات في الجدول كما يلي، ولاحظ أننا استوردنا النموذج Post بدلًا من الصنف DB، حيث سيتصل النموذج Post تلقائيًا بالجدول posts: use App\Models\Post; $posts = Post::all(); بما أن النموذج هو باني استعلامات، لذا يمكن أن تصل النماذج إلى جميع توابع باني الاستعلامات كما يلي: $posts = Post::where('published', true) ->orderBy('title') ->take(10) ->get(); إذا استخدمتَ التابعين all() أو get() لاسترداد البيانات، فلن تكون القيمة المُعادة مصفوفةً أو كائنًا بسيطًا، ولكنها ستكون نسخة من الصنف Illuminate\Database\Eloquent\Collection، حيث يوفّر الصنف Collection عدة توابع أقوى من الكائنات البسيطة. يمكن استخدام التابع find() مثلًا لتحديد موقع سجل باستخدام مفتاح رئيسي هو id عادة. $posts = Post::all(); $post = $posts->find(1); سيكون $post في هذا المثال هو المنشور ذو المعرّف id==1. إدراج وتحديث النموذج يمكننا أيضًا إدراج السجلات أو تحديثها باستخدام النموذج، ولكن تأكد أولًا من أن النموذج المقابل له الخاصية $fillable وتأكد من سرد جميع الأعمدة التي يجب أن تكون قابلة للملء. class Post extends Model { /** * السمات Attributes التي يمكن إسنادها بطريقة جماعية * * @var array */ protected $fillable = ['title', 'content']; } يمكننا بعد ذلك استخدام التابع create() لإنشاء سجل جديد كما يلي: $flight = Post::create([ 'title' => '. . .', 'content' => '. . .', ]); أو يمكننا تحديث السجلات الموجودة مسبقًا باستخدام التابع update(): Flight::where('published', true)->update(['published' => false]); حذف النموذج يوجد تابعان يسمحان بحذف السجلات، حيث إذا أردتَ حذف سجل واحد، فاستخدم التابع delete(): $posts = Post::all(); $post = $posts->find(1); $post->delete(); وإذا أردتَ حذف السجلات بطريقة جماعية، فاستخدم التابع truncate(): $posts = Post::where('published', true) ->orderBy('title') ->take(10) ->get(); $posts->truncate(); علاقات قاعدة البيانات لا تكون جداول قاعدة البيانات في تطبيقات الويب مستقلة بل تكون مترابطة بعلاقات فيما بينها، فمثلًا يمكن أن يكون لدينا مستخدم لديه منشورات ويمكن أن يكون لدينا منشوراتٌ تخص مستخدمًا ما، لذا يقدّم لارافيل طريقة لتعريف هذه العلاقات باستخدام نماذج Eloquent. يُحتمَل أن يكون القسم التالي صعبًا بعض الشيء بالنسبة للمبتدئين، ولكن لا تقلق بهذا الشأن، إذ سنعود إلى هذا الموضوع لاحقًا عندما نبدأ في إنشاء تطبيق المدونة، حيث سنوضّح فقط ما نحتاج إلى استخدامه لبناء نظام تدوين بسيط. علاقة واحد إلى واحد تُعَد علاقة واحد إلى واحد One to One العلاقة الأساسية، فمثلًا يرتبط كل مستخدم User بحيوان أليف Pet واحد. يمكن تعريف هذه العلاقة من خلال وضع التابع pet في النموذج User كما يلي: <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * الحصول على الحيوان الأليف المرتبط بالمستخدم */ public function pet() { return $this->hasOne(Pet::class); } } إذا أردنا أن نطبّق العكس، حيث يكون معكوس العلاقة "يملك" has one هو "يعود إلى" belongs to one، مما يعني أن كل حيوان أليف Pet يعود إلى مستخدم User واحد، ويمكن تعريف معكوس العلاقة واحد إلى واحد من خلال وضع التابع user في النموذج Pet كما يلي: class Pet extends Model { /** * الحصول على المستخدم الذي يملك الحيوان الأليف */ public function user() { return $this->belongsTo(User::class); } } لكننا لم ننتهِ بعد، إذ يجب أيضًا إجراء بعض التغييرات على جداول قاعدة البيانات المتقابلة، إذ يفترض النموذج User وجود العمود pet_id ضمن الجدول users، ويخزّن معرّف id الحيوان الأليف الذي يملكه المستخدم. يجب أيضًا إجراء تعديلات على الجدول pets المقابل، إذ يجب وجود العمود user_id الذي يخزّن معرّف id المستخدم الذي يعود إليه هذا الحيوان الأليف. علاقة واحد إلى متعدد تُستخدم علاقة واحد إلى متعدد One-to-Many لتعريف العلاقات التي يمتلك فيها نموذج واحد نسخًا متعددة من نموذج آخر، فمثلًا يمكن أن تحتوي فئة Category واحدة على العديد من المنشورات posts، ويمكن تعريف هذه العلاقة من خلال وضع التابع posts في النموذج Category كما يلي: class Category extends Model { public function posts() { return $this->hasMany(Post::class); } } ولكن يجب في بعض الأحيان العثور على الفئة عبر المنشور، فمعكوس العلاقة "لديه العديد" has many هو "ينتمي إلى" belongs to، ويمكن تعريف معكوس العلاقة واحد إلى متعدد من خلال وضع التابع category في النموذج Post كما يلي: class Post extends Model { public function category() { return $this->belongsTo(Category::class); } } تفترض هذه العلاقة أن الجدول posts يحتوي على العمود category_id الذي يخزّن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تُعَد العلاقة متعدد إلى متعدد Many-to-Many أصعب بعض الشيء، فمثلًا يمكن أن يكون لدينا مستخدم User لديه العديد من الأدوار، ويمكن أن يكون لدينا دور Role له العديد من المستخدمين كما يلي: class User extends Model { /** * الأدوار التي تخص المستخدم */ public function roles() { return $this->belongsToMany(Role::class); } } class Role extends Model { /** * المستخدمون الذين ينتمون إلى الدور */ public function users() { return $this->belongsToMany(User::class); } } تفترض هذه العلاقة وجود الجدول role_user في قاعدة البيانات واحتواء الجدول role_user على العمودين user_id و role_id، وبالتالي يمكننا مطابقة المستخدم مع دوره والعكس صحيح. لنفترض أن لدينا الجدول role_user كما يلي: user_id role_id 1 1 2 1 3 2 1 2 2 3 يمتلك المستخدم الذي له المعرّف id=1 دورَين لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمر للعثور على المستخدمين عبر الدور، فيمكننا أن نرى مستخدمَين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للدور ذي المعرّف id=2. طبقة المتحكم Controller وضّحنا الوِجهات والعروض والنماذج وعلاقات قاعدة البيانات، وحان الوقت الآن لنتعرّف على الشيء الذي يربطها جميعًا مع بعضها البعض. تحدّثنا في المقال السابق عن الوِجهات، واستخدمنا المثال التالي: Route::get('/number/{num}', function ($num) { return view('view', ['num' => $num]); }); يبدو الأمر جيدًا في الأمثلة البسيطة، ولكن إذا كان لديك مشروع ضخم، فسيؤدي وضع كل الشيفرة البرمجية في ملف الوِجهة Route File إلى جعل الأمر فوضويًا جدًا، والحل الأفضل هو التأكّد من أن الوِجهة تؤشّر دائمًا إلى تابع في المتحكم مع وضع الشيفرة البرمجية ضمن هذا التابع. use App\Http\Controllers\UserController; Route::get('/users/{name}', [UserController::class, 'show']); يمكن إنشاء متحكم جديد من خلال استخدام الأمر التالي: php artisan make:controller UserController تُخزَّن ملفات متحكم لارافيل ضمن المجلد app/Http/Controllers/. المتحكم الأساسي لنلقِ نظرة على المثال التالي، حيث يتوقع التابع show() المتغير $id، ويستخدِم هذا المعرّف id لتحديد موقع المستخدم في قاعدة البيانات، ويعيد هذا المستخدم في العرض. <?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\View\View; class UserController extends Controller { /** * عرض الملف الشخصي لمستخدمٍ معين */ public function show(string $name): View { return view('user.profile', [ 'user' => User::firstWhere('name', $name); ]); } } تحتاج في بعض الأحيان إلى تعريف متحكم له تابع واحد فقط، ويمكننا في هذه الحالة تبسيط الشيفرة البرمجية من خلال تغيير اسم التابع إلى _invoke. class UserController extends Controller { public function __invoke(string $name): View { return view('user.profile', [ 'user' => User::firstWhere('name', $name); ]); } } لا حاجة الآن لتحديد اسم التابع في الوِجهة كما يلي: Route::get('/users/{id}', UserController::class); متحكم الموارد المورد Resource هو مفهوم حديث آخر لتصميم الويب، حيث نرى جميع نماذج Eloquent بوصفها مواردًا، مما يعني أننا سنجري مجموعة الإجراءات نفسها عليها جميعًا، وهذه الإجراءات هي الإنشاء Create والقراءة Read والتحديث Update والحذف Delete -أو CRUD اختصارًا. يمكن إنشاء متحكم موارد من خلال استخدام الخيار --resource كما يلي: php artisan make:controller PostController --resource سيحتوي المتحكم الذي أنشأناه على التوابع التالية تلقائيًا: <?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): Response { // } /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): Response { // } /** * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // } /** * عرض المورد المُحدَّد */ public function show(string $id): Response { // } /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): Response { // } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // } /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { // } } تشرح التعليقات في الشيفرة البرمجية السابقة وظيفة التوابع والغرض منها. يقدّم لارافيل أيضًا طريقة سهلة جدًا لتسجيل الوِجهات لكل من التوابع السابقة كما يلي: Route::resource('posts', PostController::class); ستَنشَأ الوِجهات التالية باستخدام توابع HTTP المختلفة تلقائيًا: تابع HTTP مسار URI الإجراء اسم الوِجهة GET /posts index posts.index GET /posts/create create posts.create POST /posts store posts.store GET /posts/{post} show posts.show GET /posts/{post}/edit edit posts.edit PUT/PATCH /posts/{post} update posts.update DELETE /posts/{post} destroy posts.destroy ترجمة -وبتصرُّف- للمقال Laravel for Beginners #2 - The MVC Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: لارافيل للمبتدئين-الجزء الأول: البدء في إنشاء مدونة بسيطة كيف تستخدم منشئ الاستعلامات Query builder للتخاطب مع قاعدة البيانات في Laravel تعرف على إطار عمل تطوير الويب لارافيل Laravel تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder علاقات Eloquent والتحميل الحثيث في Laravel 5
-
يُعَد الجداء الشعاعي عمليةً تستخدم شعاعين على أساس معامَلين. وتكمن أهمية هذا في الرسوميات الحاسوبية عند محاولة العثور على شعاع عمودي على مستوٍ؛ ويُعَد ذلك ضروريًا عند حساب كيفية انعكاس الضوء على السطوح. سنوضّح في هذا المقال المواضيع التالية: تعريف الجداء الشعاعي لشعاعين. قاعدة اليد اليمنى: يكون منحى Orientation الجداء الشعاعي u × v باتجاه "الإبهام" إذا طويت أصابعك من الشعاع u إلى الشعاع v. خاصيات الجداء الشعاعي والتي هي: حجم Magnitude الجداء الشعاعي: حجم الجداء الشعاعي u × v هو مساحة متوازي الأضلاع التي يحدّدها هذان الشعاعان. لا يُعَد الجداء الشعاعي عملية تبديلية Anti-commutative: u × v = -(v × u) u × u = 0 و (ku) × u = 0 نتيجة الجداء الشعاعي لشعاع مع شعاع صفري هي شعاع صفري: u × 0 = 0 × u = 0 لا يُعَد الجداء الشعاعي عملية تجميعية: (u × v) × w ≠ u × (v × w) إمكانية توزيع الجداء الشعاعي على جمع الأشعة: u × (v + w) = u × v + u × w الجداء الشعاعي بين محاور الإحداثيات: i × j = k و j × k = i و k × i = j حساب الجداء الشعاعي باستخدام المصفوفات العمودية. استخدام أداة مساعدة شبيهة بمحدّدات المصفوفات Determinant لتذكّر صيغة حساب الجداء الشعاعي للمصفوفات العمودية. تذكير: يكون نوع الكائنات التي تمثّلها معامَلات الجداء النقطي أشعةً، وتكون نتيجة الجداء النقطي عددًا حقيقيًا (مقدارًا سلميًا) Scalar. لقد شاهدت حتى الآن العمليات التالية: ولكن لا توجد عملية تشبه الضرب وتأخذ شعاعين كمعامَلين وينتج عنها شعاع، وهذا ما يمثله جداء الأشعة الشعاعي الذي سنشرحه في هذا المقال. تعريف الجداء الشعاعي إذا كان u و v شعاعين في فضاء ثلاثي الأبعاد، فإن الجداء الشعاعي لهما u × v هو شعاع ثلاثي الأبعاد، ويجب أن تكون معاملات هذه العملية ونتيجتها أشعةً ثلاثية الأبعاد على عكس العمليات الأخرى؛ إذ أن عملية الجداء الشعاعي ليست مُعرَّفةً في الفضاء ثنائي الأبعاد 2D. يمكن تعريف الجداء الشعاعي لشعاعين u × v كما يلي: يجب أن يكون u و v شعاعين ثلاثي الأبعاد. والنتيجة هي شعاع ثلاثي الأبعاد مع الطول والمنحى التاليين: الطول: | u × v | = | u | | v | sin θ حيث θ هي الزاوية بين الشعاعين u و v. المنحى: يكون ناتج الجداء الشعاعي u × v عموديًا على كلٍّ من الشعاعي u و v. يجري الاختيار من بين منحيين متعامدين على الشعاعين u و v باستخدام قاعدة اليد اليمنى. ناتج الجداء الشعاعي لشعاعين هو شعاع عمودي على هذه الشعاعين، ولكن هناك اتجاهين متعامدين على كلٍّ من هذين المعامَلين، حيث يمكن الاختيار من بينهما باستخدام قاعدة اليد اليمنى، والتي سنشرحها في الفقرة التالية. قاعدة اليد اليمنى Right-hand Rule نضع البرغي الخشبي عموديًا على لوحٍ من الخشب، ونلفّه باتجاه اللوح الخشبي باستخدام مفك براغي؛ إذ يجب أن ندير البرغي اليميني (النوع العادي) باتجاه عقارب الساعة من رأسه إلى اللوح الخشب. يكون الجداء الشعاعي لشعاعين متعامدًا عليهما، وتختار قاعدة اليد اليمنى أحد الاتجاهين المتعامدين المُحتمَلين. ويوضح الرسمان البيانيان السابقان هذه القاعدة. لا تنسَ أن ترتيب المعاملات مهم، إذ تؤشّر نتيجة الجداء الشعاعي u × v إلى الاتجاه نفسه الذي يجب أن يؤشّر إليه البرغي، إذا دفع الالتفافُ من الشعاع u إلى الشعاع v البرغي إلى داخل قطعة الخشب، أو فكّر في الاتجاه الذي يؤشّر إليه إبهامك عندما تطوي أصابعك من الشعاع u إلى الشعاع v. ملاحظة: يؤشّر الجداء الشعاعي u × v و v × u إلى اتجاهين متعاكسين. تصور الجداء الشعاعي يُظهر الشكل التالي الشعاعين u و v ومتوازي الأضلاع المُحدَّد بينهما. مساحة متوازي الأضلاع هي: | u | | v | sin θ، حيث θ هي الزاوية بينهما. تمثّل هذه المساحة حجم الجداء الشعاعي، وتكون هذه الحقيقة مفيدةً أحيانًا لتصور الجداء الشعاعي، فإذا دوّرنا الشعاع u مثلًا، بحيث يقترب منحاه من منحى الشعاع v (دون تغيير طوله)، فستقترب مساحة متوازي الأضلاع وحجم الجداء الشعاعي من الصفر، بينما تعطي الزاوية 90 درجة بين الشعاعين u و v القيمة العظمى للجداء الشعاعي. عكس ترتيب معاملات الجداء الشعاعي تذكير: يمكن عكس اتجاه الشعاع w من خلال ضربه بالعدد -1؛ إذ يؤشّر الشعاعان w و -w إلى اتجاهين متعاكسين. يوضح الشكل السابق حاصل عمليتي جداء شعاعين حدّدنا منحاهما باستخدام قاعدة اليد اليمنى، ويكون حجم الجداء الشعاعي: u × v = | u | | v | sin θ = | v | | u | sin θ وبالتالي: u × v = -(v × u) ويمكن القول أن: -(v × u) = -v × u المعاملات المتوازية للجداء الشعاعي طول u × v هو | u | | v | sin θ، وإذا كان u و v شعاعين على استقامة واحدة Collinear (متوازيين)، فإن الزاوية بينهما θ = 0، وبالتالي سيكون sin θ = 0، وحجم ناتج الجداء الشعاعي يساوي الصفر، ولكن النتيجة هي شعاع؛ وبالتالي تكون نتيجة الجداء الشعاعي هي الشعاع الصفري 0. u × u = 0 بما أن k u على استقامة واحدة مع الشعاع u (حيث k عدد حقيقي)، فسيكون: (k u) × u = 0 نتيجة الجداء الشعاعي u × u = 0 عمودية على كلا المعامَلين، لأن 0 · u = 0، مما يعني أنهما متعامدان؛ فالشعاع الصفري 0 عمودي على جميع الأشعة كما مر معنا سابقًا. حجم الجداء الشعاعي u × 0 وهو | u | | 0 | sin θ يساوي الصفر، والنتيجة هي شعاع، وبالتالي: u × 0 = 0 × u = 0 لنوجد الآن الجداء الشعاعي (k u) × v من خلال الاطلاع على الرسم البياني السابق، أو من خلال العمل بالصيغة التالية: (k u) × v = k( u × v ) وسينتج ما يلي: | (k u) × v | = | k u | | v | sin θ = | k | | u | | v | sin θ إذًا يكون حجم (k u) × v أكبر بمقدار | k | عن حجم u × v مع إبقاء الاتجاه نفسه. ليس الجداء الشعاعي للأشعة عملية تجميعية لا يُعَد الجداء الشعاعي للأشعة عمليةً تجميعية، ويمكن رؤية ذلك من خلال النظر إلى الرسم البياني السابق، ثم تشكيل الجداء الشعاعي (u × v)، ثم إجراء الجداء الشعاعي للناتج مع الشعاع w. نشكّل بعد ذلك حاصل الجداء الشعاعي (v × w)، ثم نجري الجداء الشعاعي للناتج مع الشعاع u؛ وينتج لدينا ما يلي: (u × v) × w ≠ u × (v × w) لاحظ أن ناتج الجداء الشعاعي للشعاعين u × v من الرسم البياني السابق يساوي الشعاع الصفري 0، لأنهما على استقامة واحدة. إمكانية توزيع الجداء الشعاعي على جمع الأشعة توجد خاصية أخرى للجداء الشعاعي، وهي إمكانية توزيع الجداء الشعاعي على عملية جمع الأشعة كما يلي: u × (v + w) = u × v + u × w ويمكن الوصول إلى هذه الخاصية من خلال التعويض في الصيغة التي تعرّف الجداء الشعاعي. الجداء الشعاعي بين محاور الإحداثيات تسمَّى أشعة الوحدة Unit Vectors التي تكون باتجاه المحاور x و y و z (في أيّ إطار إحداثي مُستخدَم)، i و j و k في أغلب الأحيان، وتوضَع أحيانًا قبعات صغيرة على كل حرف كما في الشكل التالي: بما أن طول كل شعاع من هذه الأشعة يساوي 1، وهي أشعة متعامدة فيما بينها، فإن جيب Sine الزاوية بين أيّ شعاعين منها يساوي 1.0، وستكون الصيغ التالية صحيحة: i × j = k j × k = i k × i = j i × k = - j k × j = - i j × i = - k i × i = 0 j × j = 0 k × k = 0 ولكن يُفضَّل رسم الرسم البياني ثم استخدام قاعدة اليد اليمنى بدلًا من حفظها. الجداء الشعاعي للمصفوفات العمودية نعرّف الجداء الشعاعي لمصفوفتين عموديتين u × v كما يلي: يجب أن تمثّل المصفوفتان العموديتان u و v أشعة ثلاثية الأبعاد. تمثّل النتيجة شعاعًا ثلاثي الأبعاد. إذا مثّلنا الشعاع u بالمصفوفة العمودية: u = (ui, uj, uk)T ومثّلنا الشعاع v بالمصفوفة العمودية: v = (vi, vj, vk)T فإن: u × v = ( uj vk - uk vj, uk vi - ui vk , ui vj - uj vi )T ملاحظة: لا تفكر في حفظ النتيجة السابقة، بل حاول إيجاد نمط محدّد لتشكيلها. لنوجد الآن الجداء الشعاعي (1, 2, 3)T × (0, 0, 0)T: لاحظ أن: (1, 2, 3)T × 0 = 0 أو يمكنك التعويض في صيغة الجداء الشعاعي للمصفوفات العمودية: (1, 2, 3)T × (0, 0, 0)T = ( 2×0 - 3×0, 3×0 - 1×0, 1×0 - 2×0)T = 0 طريقة مساعدة لتذكر صيغة الجداء الشعاعي للمصفوفات العمودية سنوضّح الآن طريقة لحساب الجداء الشعاعي من خلال ترتيب عناصر كل شعاع في محدّد مصفوفة Determinant، والذي يساعدك لتذكر صيغة الجداء الشعاعي السابقة الصعبة جدًا، ولكن إذا كنت تعرف مسبقًا كيفية تقييم المحدّدات ذات الحجم 3x3، فيمكنك تخطي هذه الطريقة، واستخدام الصيغة الموجودة في التعريف. يحتوي الصف العلوي من المحدّد على الرموز التي تمثل كل محور، ولكن تذكّر أن هذه الطريقة لا تمثّل تعريف الجداء الشعاعي، ولا تمثّل محدد مصفوفة، ولكنها مجرد أداة مساعدة للتذكر. تعمل هذه الطريقة فقط مع الأشعة ثلاثية الأبعاد، إذ لم يُعرَّف الجداء الشعاعي إلّا للأشعة في الفضاء ثلاثي الأبعاد، لذا ضع مكونات الشعاع الأول في الصف الثاني من المحدّد، ومكونات الشعاع الثاني في الصف الثالث. تدريب عملي تدريب1: لنوجِد الجداء الشعاعي للمصفوفتين العموديتين (1, 2, 1)T و (0, -1, 2)T كما يلي: ولنقيّم الآن النتيجة من خلال البحث عن العوامل المساعدة للمكوّن i و j و k التي أحطناها بدائرة حمراء في الصف العلوي؛ فالعوامل المساعدة للمكوّن i مثلًا هي عناصر الصفين 2 و 3 التي ليست موجودة في العمود i. يمكن إيجاد العامل المساعد من خلال ضرب العنصرين الموجودين على القطر الرئيسي، ثم طرح حاصل ضرب العنصرين الموجودين على القطر الآخر من نتيجة ضرب العنصرين الموجودين على القطر الرئيسي، فعلى سبيل المثال، يكون العامل المساعد للمكوّن i هو 2×2 - 1×(-1) = 2 + 1 = 5. تدريب2: يكون حاصل الجداء الشعاعي للمصفوفتين العموديتين (0, -1, 2)T و (1, 2, 1)T كما يلي (لاحظ أننا عكسنا ترتيب الشعاعين السابقين): يتبع تقييم هذا المثال نمط المثال السابق نفسه، ولكن مع تبديل الصفين الثاني والثالث كما يلي: لاحظ أن النتيجة تمثّل معاكس النتيجة السابقة، مما يدل على أن: u × v = -(v × u) ولقد رأينا ذلك سابقًا عندما استخدمنا الأشعة الهندسية. الجداء الشعاعي لمصفوفة عمودية مع نفسها يبقى الصفان الثاني والثالث كما هما عندما نأخذ الجداء الشعاع لشعاعٍ مع نفسه، وتذكّر من دروس الرياضيات في المرحلة الثانوية أنه إذا كان صفان من المحدّد متماثلين، فستكون قيمته صفرًا؛ حيث يكون كل عامل مساعد صفرًا، مما يؤدي إلى أن: 0 i + 0 j + 0 k = 0 يحدث الشيء نفسه إذا كان أحد الصفوف مضاعفًا للآخر؛ إذ تعكس هذه النتائج ما رأيناه مسبقًا مع الأشعة الهندسية: k u × u = 0 تنطبق أيضًا الخاصيات الأخرى لحاصل الجداء الشعاعي للأشعة الهندسية على حاصل الجداء الشعاعي لتمثيلاتها باستخدام المصفوفات العمودية. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية حساب الجداء الشعاعي للأشعة، وسنتعرّف في المقال التالي على المصفوفات المستطيلة وعملياتها. ترجمة -وبتصرُّف- للفصل Vector Cross Product من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: إسقاط شعاع على شعاع آخر واستخداماته في الرسوميات الحاسوبية 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D
-
يمكن أن تخدع صيغة بايثون البسيطة وسهلة التعلم مطوري لغة بايثون Python وخاصة الجدد منهم، مما يؤدي إلى تفويت بعض التفاصيل الدقيقة والتقليل من قوة اللغة، لذا سنقدّم في هذا المقال قائمة بأكثر 10 أخطاء شيوعًا، والتي تكون دقيقة ويصعب اكتشافها ويمكن أن تخدع حتى مطور بايثون الأكثر تقدمًا. مقدمة إلى بايثون تُعَد بايثون لغة برمجة مُفسَّرة Interpreted وكائنية التوجّه Object-oriented وعالية المستوى ولها دلالات Semantics ديناميكية، وتجعل هياكلُ البيانات المُضمَّنة عالية المستوى، والتحقق الديناميكي من الأنواع، والربط الديناميكي من لغة بايثون جذابة للغاية لتطوير التطبيقات بسرعة، بالإضافة إلى استخدامها بوصفها لغة برمجة لكتابة السكربتات أو لغة لاصقة Glue Language لوصل المكونات أو الخدمات الموجودة مسبقًا مع بعضها البعض. كما تدعم لغة بايثون الوحدات والحزم، وبالتالي تشجع التقسيم إلى وحدات Modularity وإعادة استخدام الشيفرة البرمجية. ملاحظة: هذا المقال مخصَّص للمبرمجين المحترفين في بايثون، وليس موجَّهًا للمطورين الجدد الذين قد يكونون أقل دراية بأخطاء بايثون الشائعة. الخطأ 1: استخدام التعابير بوصفها قيمًا افتراضية لوسطاء الدوال بطريقة خاطئة تسمح لغة بايثون بتحديد وسيط الدالة بأنه اختياري من خلال توفير قيمة افتراضية له، ولكن قد تؤدي هذه الميزة إلى بعض الارتباك عندما تكون القيمة الافتراضية متغيرة بالرغم من أن هذه ميزة رائعة لهذه اللغة. إليك تعريف دالة بايثون التالي مثلًا: >>> def foo(bar=[]): # يُعد الوسيط bar اختياريًا وقيمته الافتراضية هي [] عند عدم تحديدها ... bar.append("baz") # ولكن يمكن أن يسبّب هذا السطر مشكلة كما سنرى لاحقًا... ... return bar من الأخطاء الشائعة أن نعتقد أن الوسيط الاختياري مضبوط على التعبير الافتراضي المحدَّد في كل مرة تُستدعَى فيها الدالة دون توفير قيمة لهذا الوسيط الاختياري، فمثلًا قد نتوقع في الشيفرة البرمجية السابقة أن استدعاء الدالة foo() بصورة متكررة (أي بدون تحديد الوسيط bar) سيؤدي دائمًا إلى إعادة القيمة 'baz'، بما أننا اعتقدنا أن الوسيط bar مضبوط على القيمة [] (أي قائمة فارغة جديدة) في كل مرة نستدعي فيها الدالة foo() (بدون تحديد الوسيط bar)، ولكن لنلقِ نظرة على ما يحدث فعليًا: >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"] لاحظ استمرار إلحاق القيمة الافتراضية "baz" إلى القائمة الموجودة مسبقًا في كل مرة نستدعي فيها الدالة foo() بدلًا من إنشاء قائمة جديدة في كل مرة، إذ تُقيَّم القيمة الافتراضية لوسيط الدالة مرة واحدة فقط في وقت تعريف الدالة، وبالتالي يُهيَّأ الوسيط bar على قيمته الافتراضية (أي قائمة فارغة) عند تعريف الدالة foo() لأول مرة فقط، ولكن ستستمر بعد ذلك استدعاءات الدالة foo() (بدون تحديد الوسيط bar) في استخدام القائمة نفسها التي هيّأنا بها الوسيط bar في الأصل. الحل الشائع لهذه المشكلة هو ما يلي: >>> def foo(bar=None): ... if bar is None: # أو if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"] الخطأ 2: استخدام متغيرات الصنف Class استخدامًا خاطئًا ليكن لدينا المثال التالي: >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1 وبالتالي سيكون لدينا أيضًا ما يلي كما هو متوقع: >>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1 ولكن سيكون لدينا ما يلي: >>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3 لاحظ تغيير قيمة C.x بالرغم من أننا غيرنا قيمة A.x فقط، حيث تُعامَل متغيرات الصنف داخليًا على أنها قواميس في لغة بايثون وتتبع ما يشار إليه غالبًا باسم ترتيب تحليل التوابع أو ترتيب استبيان التوابع Method Resolution Order -أو MRO اختصارًا وهو الآلية التي تستخدمها لغات البرمجة ومن ضمنها بايثون لتحديد ترتيب البحث عن التوابع في التسلسل الهرمي hierarchy الخاص بالكائنات في حالة استخدام الوراثة المتعددة، أي أنه يحدد المسار الذي سيتبعه البرنامج عند محاولة استدعاء دالة معينة موجودة في أكثر من صنف أو الوراثة من عدة أصناف. لذلك سنبحث عن السمة Attribute التي هي x في أصنافها الأساسية (أي الصنف A فقط في المثال السابق بالرغم من أن لغة بايثون تدعم الوراثة المتعددة) بما أننا لم نعثر على هذه السمة في الصنف C. يمكن القول أيضًا أن الصنف C ليس لديه الخاصية x الخاصة به والمستقلة عن الصنف A، وبالتالي لا يُعَد المرجع إلى C.x هو المرجع نفسه إلى A.x، ويؤدي ذلك إلى حدوث مشكلة في بايثون إن لم نتعامل معها بطريقة صحيحة. الخطأ 3: تحديد المعاملات لكتلة الاستثناء Exception بطريقة خاطئة لنفترض أن لدينا الشيفرة البرمجية التالية: >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # لالتقاط الاستثناءَين ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range المشكلة في المثال السابق هي أن تعليمة except لا تأخذ قائمة الاستثناءات المُحدَّدة بهذه الطريقة، حيث تستخدم بايثون الصيغة except Exception, e لربط الاستثناء بالمعامل الثاني الاختياري المُحدَّد (هو e في هذه الحالة)، وبالتالي يمكن إتاحته لمزيد من الفحص. لم تلتقط التعليمة except الاستثناء IndexError، بل يُربَط الاستثناء بمعاملٍ اسمه IndexError، وتُعَد مثل هذه الأخطاء شائعة في شيفرة بايثون البرمجية. الطريقة الصحيحة لالتقاط الاستثناءات المتعددة في التعليمة except هي تحديد المعامل الأول بوصفه مجموعة Tuple تحتوي على جميع الاستثناءات المُلتقَطة. يمكن تحقيق أقصى قدر من قابلية النقل من خلال استخدام الكلمة المفتاحية as لأن هذه الصيغة تدعمها Python 2 و Python 3: >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>> الخطأ 4: سوء فهم قواعد نطاق Scope بايثون يعتمد تحليل Resolution نطاق بايثون على قاعدة LEGB، وهي اختصار للكلمات محلي Local وشامل Enclosing وعام Global ومُضمَّن Built-in. توجد بعض التفاصيل الدقيقة للطريقة التي تعمل بها هذه القاعدة في بايثون، مما يقودنا إلى مشكلة برمجة بايثون الشائعة الأكثر تقدمًا التالية، فليكن لدينا ما يلي مثلًا: >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment يحدث الخطأ السابق لأنه إذا أسندتَ قيمة إلى متغير في نطاقٍ ما، فستَعُد لغة بايثون هذا المتغير متغيرًا محليًا لذلك النطاق تلقائيًا وتظلّل أيّ متغير يحمل الاسم نفسه في أيّ نطاق خارجي. يتفاجأ الكثير من المبرمجين بالحصول على الخطأ UnboundLocalError في الشيفرة البرمجية التي عملت بنجاح سابقًا عند تعديلها من خلال إضافة تعليمة إسناد في مكانٍ ما من جسم الدالة، فمن الشائع أن يؤدي ذلك إلى أن يخطئ المطورون عند استخدام القوائم خاصةً كما في المثال التالي: >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # تعمل هذه التعليمة بنجاح ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ولكن تعطي هذه التعليمة خطأً ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment لاحظ أن الدالة foo1 تعمل بنجاح بينما تعطي الدالة foo2 خطأ، والسبب في ذلك هو مماثل لمشكلة المثال السابق ولكنه أكثر دقة، حيث لا تسند الدالة foo1 قيمة إلى المتغير lst على عكس الدالة foo2، فالتعليمة lst += [5] هي مجرد اختصار للتعليمة lst = lst + [5] التي تمثّل محاولة إسناد قيمة إلى المتغير lst، وبالتالي تفترض لغة بايثون أن هذا المتغير موجود في النطاق المحلي، ولكن تعتمد القيمة التي نريد إسنادها إلى المتغير lst على المتغير lst نفسه الذي يُفترَض وجوده في النطاق المحلي ولم نعرّفه بعد. الخطأ 5: تعديل القائمة أثناء المرور عليها يجب أن تكون مشكلة الشيفرة البرمجية التالية واضحة إلى حدٍ ما: >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # تصرف سيء: حذف عنصر من القائمة أثناء المرور عليها ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range يُعَد حذف عنصر من قائمة أو مصفوفة أثناء المرور عليها مشكلةً معروفةً جيدًا في بايثون لمطوري البرمجيات أصحاب الخبرة، ولكن بالرغم من أن المثال السابق قد يكون واضحًا إلى حدٍ ما، إلّا أنه يمكن أن يرتكب حتى المطورون المتقدمون عن غير قصد هذا الخطأ في الشيفرة البرمجية الأكثر تعقيدًا. لحسن الحظ، تتضمن لغة بايثون عددًا من نماذج البرمجة الأنيقة التي يمكن أن تؤدي إلى شيفرة برمجية مبسطة ومنظَّمة بصورة كبيرة عند استخدامها استخدامًا صحيحًا، ممّا يقلل من احتمالية وجود خطأ الحذف غير المقصود لعنصر القائمة أثناء المرور عليها في هذه الشيفرة البرمجية الأبسط. أحد هذه النماذج هو نموذج استيعاب القوائم List Comprehensions الذي يُعَد مفيدًا خاصةً لتجنب هذه المشكلة كما هو موضّح في التطبيق البديل التالي للشيفرة البرمجية السابقة والذي يعمل بطريقة مثالية: >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # الحل هنا >>> numbers [0, 2, 4, 6, 8] الخطأ 6: عدم وضوح كيفية ربط Bind بايثون للمتغيرات في المنغلقات Closures ليكن لدينا المثال التالي: >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ... قد نتوقع الخرج التالي للشيفرة البرمجية السابقة: 0 2 4 6 8 لكننا سنحصل على ما يلي: 8 8 8 8 8 يحدث ذلك بسبب سلوك الربط المتأخر Late Binding لبايثون الذي يبحث عن قيم المتغيرات المُستخدَمة في المنغلقات Closures في وقت استدعاء الدالة الداخلية، لذلك إذا استدعينا أيًا من الدوال المُعادة في المثال السابق، فسيُجرَى البحث عن قيمة المتغير i في النطاق المحيط في وقت استدعائها، حيث ستكون الحلقة قد اكتملت عندها، لذلك أُسنِدت القيمة 4 إلى المتغير i فعليًا. ويكون حل هذه المشكلة الشائعة في بايثون كما يلي: >>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8 استفدنا من الوسطاء الافتراضية لإنشاء دوال مجهولة لتحقيق السلوك المطلوب. قد يَعُد البعض هذه الطريقة مناسبة، وقد يَعدها البعض رائعة، وقد يكرهها البعض الآخر، ولكن من المهم أن تفهمها إذا كنت مطور بايثون. الخطأ 7: إنشاء اعتماديات Dependencies الوحدات الدائرية لنفترض أن لدينا الملفان a.py و b.py، حيث يستورد كلّ منهما الآخر كما يلي: في الملف a.py: import b def f(): return b.x print f() وفي الملف b.py: import a x = 1 def g(): print a.f() أولًا، لنحاول استيراد الوحدة a.py كما يلي: >>> import a 1 لاحظ أن عملية الاستيراد نجحت، وقد يكون ذلك مفاجأة لك، فلدينا استيراد دائري هنا والذي يُفترَض أن يمثل مشكلة، أليس كذلك؟ ولكن لا يمثّل مجرد وجود استيراد دائري في حد ذاته مشكلة في بايثون، فلغة بايثون ذكية بما يكفي لعدم محاولة إعادة استيراد وحدة إذا كانت مستوردةً فعليًا، ولكنك قد تواجه مشكلات اعتمادًا على النقطة التي تحاول فيها كل وحدة الوصول إلى الدوال أو المتغيرات المُعرَّفة في الوحدة الأخرى. لم يكن هناك مشكلة في استيراد الوحدة b.py لأنها لا تتطلب تعريف أيّ شيء من الوحدة a.py في وقت استيرادها عندما استوردنا الوحدة a.py في المثال السابق، فالإشارة الوحيدة إلى الوحدة a في الملف b.py هو استدعاء الدالة a.f()، ولكن هذا الاستدعاء موجود في الدالة g() ولا يوجد شيء في الملفين a.py أو b.py يستدعي الدالة g()، لذا لا يوجد شيء يدعو للقلق. ولكن إذا حاولنا استيراد الوحدة b.py دون استيراد الوحدة a.py مسبقًا كما يلي: >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x' فستظهر مشكلة تتمثّل في أن الوحدة b.py تحاول استيراد الوحدة a.py عند عملية استيراد الوحدة b.py، وتستدعي الوحدة a.py بدورها الدالة f() التي تحاول الوصول إلى المتغير b.x الذي لم نعرّفه بعد، وبالتالي سيظهر الاستثناء AttributeError. توجد حلول مختلفة لهذا الخطأ، وسيكون أحد هذه الحلول على الأقل بسيطًا، فمثلًا عدّل الوحدة b.py لتستورد الوحدة a.py ضمن الدالة g(): x = 1 def g(): import a # ستُقيَّم هذه التعليمة عند استدعاء الدالة g() فقط print a.f() وإذا استوردناه هذه الوحدة، فسيكون كل شيء على ما يرام كما يلي: >>> import b >>> b.g() 1 # يُطبَع لأول مرة بسبب استدعاء الوحدة 'a' للتعليمة 'print f()' في النهاية 1 # يُطبَع مرة ثانية، حيث يمثّل استدعاء الدالة 'g' الخطأ 8: تعارض الأسماء مع وحدات مكتبة بايثون المعيارية تتميز لغة بايثون بوفرة وحدات المكتبات التي تأتي معها، ولكن قد يؤدي ذلك إلى الوقوع في تعارض في الأسماء بين اسم إحدى الوحدات الخاصة بك ووحدة أخرى تحمل الاسم نفسه في المكتبة المعيارية التي تأتي مع لغة بايثون إن لم تكن حذرًا، فمثلًا قد يكون لديك وحدة بالاسم email.py في شيفرتك البرمجية، والتي قد تتعارض مع وحدة المكتبة المعيارية التي تحمل الاسم نفسه. يمكن أن يؤدي ذلك إلى مشكلات خطيرة مثل استيراد مكتبة أخرى، والتي تحاول بدورها استيراد إصدارٍ من وحدة خاصة بمكتبة بايثون المعيارية، ولكن إذا كان لديك وحدة تحمل الاسم نفسه، فستستورد الحزمة الأخرى الإصدار الخاص بك عن طريق الخطأ بدلًا من الإصدار الموجود في مكتبة بايثون المعيارية، مما يؤدي إلى حدوث أخطاء، لذا يجب توخي الحذر لتجنب استخدام الأسماء نفسها الخاصة بوحدات مكتبة بايثون المعيارية. من الأسهل بالنسبة لك تغيير اسم الوحدة ضمن الحزمة الخاصة بك بدلًا من تقديم اقتراح تحسين بايثون Python Enhancement Proposal -أو PEP اختصارًا- لطلب تغيير الاسم ومحاولة الحصول على الموافقة على ذلك. الخطأ 9: الفشل في معالجة الاختلافات بين الإصدارين Python 2 و Python 3 ليكن لدينا الملف foo.py التالي مثلًا: import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad() يعمل ما يلي بنجاح في Python 2: $ python foo.py 1 key error 1 $ python foo.py 2 value error 2 ولكنه يعطي خطأً في Python 3 كما يلي: $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment المشكلة هي أنه لا يمكن الوصول إلى كائن الاستثناء خارج نطاق كتلة التعليمة except في Python 3، وإلّا فيجب الاحتفاظ بدورة مرجعية مع إطار المكدس في الذاكرة حتى تشغيل كانس المهملات Garbage Collector وإزالة المراجع من الذاكرة. إحدى الطرق لتجنب هذه المشكلة هي الاحتفاظ بمرجع إلى كائن الاستثناء خارج نطاق كتلة التعليمة except بحيث يبقى قابلًا للوصول. إليك فيما يلي نسخة من المثال السابق الذي يستخدم هذه التقنية، وبالتالي ستنتج شيفرة برمجية متوافقة مع Python 2 و Python 3: import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good() لنشغّل هذه الشيفرة البرمجية على الإصدار Py3k: $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2 اطّلع على مقال كيفية ترحيل شيفرة بايثون 2 إلى بايثون 3 لمزيد من المعلومات. الخطأ 10: استخدام التابع del بطريقة خاطئة لنفترض أن لدينا ما يلي في ملفٍ اسمه mod.py: import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle) ثم حاولنا استيراده من الملف another_mod.py كما يلي: import mod mybar = mod.Bar() فسنحصل على الاستثناء AttributeError، والسبب هو ضبط جميع متغيرات الوحدة العامة على القيمة None عند إيقاف تشغيل المفسّر Interpreter، لذلك ضُبِط الاسم foo على القيمة None عند استدعاء التابع __del__ في المثال السابق. الحل لهذه المشكلة هو استخدام الدالة atexit.register() بدلًا من ذلك، وبالتالي ستُشغَّل معالجاتك المسجَّلة قبل إيقاف تشغيل المفسِّر عندما ينتهي برنامجك من التنفيذ (أي عند الخروج منه بطريقة طبيعية). إذًا لنصلِح شيفرة mod.py البرمجية السابقة كما يلي: import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle) يوفّر هذا المثال طريقة نظيفة وموثوقة لاستدعاء أيّ دالة تنظيف مطلوبة عند إنهاء البرنامج العادي، ومن الواضح أن الأمر متروك للدالة foo.cleanup لتحديد ما يجب فعله بالكائن المرتبط بالاسم self.myhandle. مخاطر بايثون يمكن تجنبها من خلال معرفة الفروق الأساسية تُعَد بايثون لغة قوية ومرنة وتحتوي على العديد من الآليات والنماذج التي يمكن أن تحسّن الإنتاجية بصورة كبيرة، ولكن يمكن أن يكون الفهم أو التقدير المحدود لقدراتها في بعض الأحيان عائقًا أكثر من كونه فائدة كما هو الحال مع أيّ أداة أو لغة برمجية، حيث يعتقد الشخص في أن يعلم ما يكفي، ولكنه يشكّل خطرًا. سيساعد التعرف على الفروق الأساسية في لغة بايثون -مثل مشاكل البرمجة المتقدمة التي ذكرناها في هذا المقال- على تحسين استخدام اللغة مع تجنب بعض الأخطاء في بايثون. ترجمة -وبتصرُّف- للمقال The 10 Most Common Python Code Mistakes لصاحبه Martin Chikilian. اقرأ أيضًا مصطلحات شائعة مثيرة للالتباس في بايثون تنقيح أخطاء Debugging شيفرتك البرمجية باستخدام لغة بايثون كيف تستخدم منقح بايثون فهم العمليات المنطقية في بايثون 3 أشهر 10 مشكلات تواجه مبرمجي لغة جافا سكريبت JavaScript
-
يُعَد لارافيل Laravel إطار ويب يستخدم لغة PHP، وهو مجاني ومفتوح المصدر ويُستخدَم على نطاق واسع لتطوير تطبيقات الويب، كما أنه معروف بصيغته الأنيقة وأدواته المُستخدَمة للمهام الشائعة مثل التوجيه Routing والاستيثاق Authentication والتخزين المؤقت أو التخبئة Caching وقدرته على التعامل مع حركة الزوار العالية. يتبع لارافيل معمارية نموذج-عرض-متحكم Model-View-Controller (أو MVC اختصارًا)، ويتضمن دعمًا مُدمجًا لربط الكائنات بالعلاقات Object-Relational Mapping (أو ORM اختصارًا) وباني الاستعلامات، مما يسهّل التفاعل مع قواعد البيانات. يضم لارافيل مجتمعًا كبيرًا ونشطًا يوفّر مجموعةً كبيرة من البرامج التعليمية والحزم والموارد الأخرى التي يمكن للمطورين استخدامها، إذ يُعَد لارافيل أحد أطر عمل الويب الأكثر شعبية وقوة للغة PHP، وتثق به العديد من الشركات والمؤسسات لبناء تطبيقات ويب قوية وقابلة للتوسّع. سنشرح في هذا المقال الاستخدام الأساسي لإطار عمل لارافيل من خلال بناء نظام تدوين بسيط. ملاحظة: تعتمد هذه السلسلة من المقالات على الإصدار 11 من لارافيل. إنشاء مشروع لارافيل جديد لا يُعَد إعداد بيئة تطوير PHP مهمة سهلة، وخاصة إذا استخدمت أنظمة لينكس Linux، ولكن قدّم لارافيل منذ الإصدار 8.0 أداة جديدة اسمها Sail، والتي توفر حلًا سهلًا لتشغيل تطبيقات لارافيل من خلال استخدام أداة دوكر Docker بغض النظر عن نظام التشغيل الذي تستخدمه، طالما أن هذه الأداة مُثبَّتة لديك. أنشئ أولًا مجلد عمل جديد بالاسم laravel-tutorial مثلًا، ثم افتحه في محرّر الأكواد الذي تفضله وليكن VS Code، ونفّذ الأمر الآتي لإنشاء مشروع لارافيل جديد، وتأكّد من تشغيل دوكر قبل تنفيذ هذا الأمر، إذ قد تستغرق هذه العملية من 5 إلى 10 دقائق حتى الاكتمال: curl -s https://laravel.build/<app_name> | bash ملاحظة: إذا كنت تعمل على نظام ويندوز Windows، فتأكّد من تشغيل هذا الأمر ضمن نظام ويندوز الفرعي لنظام التشغيل لينكس WSL 2 الذي يسمح لك بتشغيل نواة Linux كاملة داخل ويندوز دون الحاجة إلى تثبيت نظام لينكس. ثانيًا، استخدم الأمر التالي للانتقال إلى مجلد التطبيق وشغّل الخادم: cd <app_name> سيؤدي الأمر التالي إلى بدء حاوية دوكر بالإضافة إلى خادم تطوير لارافيل: ./vendor/bin/sail up يمكنك الوصول إلى تطبيق لارافيل على العنوان http://localhost/، وإن لم يعمل هذا العنوان، فحاول الانتقال إلى العنوان http://127.0.0.1/ بدلًا من ذلك، ثم يجب أن تشاهد صفحة ترحيب لارافيل التالية: استكشاف بنية تطبيق لارافيل لنلقِ نظرة على المجلدات والملفات التي تنشأ ضمن مشروعنا قبل أن نبدأ ببرمجته، لذا إليك نظرة عامة على المجلد الجذر للمشروع: . ├── app │ ├── Console │ ├── Exceptions │ ├── Http │ │ ├── Controllers │ │ └── Middleware │ ├── Models │ └── Providers ├── bootstrap ├── config ├── database │ ├── factories │ ├── migrations │ └── seeders ├── public ├── resources │ ├── css │ ├── js │ └── views ├── routes ├── storage │ ├── app │ ├── framework │ └── logs ├── tests │ ├── Feature │ └── Unit └── vendor المجلد app: يُعَد هذا المجلد المكون الأساسي لمشروعنا، ويحتوي على مجلدات متعددة من أهمها مجلد المتحكمات controllers والبرمجيات الوسيطة middleware والنماذج models، حيث يعرّف المتحكم الشيفرة البرمجية الأساسية للتطبيق، وتعرّف البرمجيات الوسيطة الإجراءات التي يجب اتخاذها قبل استدعاء المتحكم، ويوفّر النموذج واجهة تسمح لنا بالتعامل مع قواعد البيانات، إذ سنتحدث عن كل منها بالتفصيل لاحقًا. المجلد bootstrap: يحتوي هذا المجلد على الملف app.php الذي يشغّل المشروع بأكمله، ولا حاجة لتعديل أيّ شيء في هذا المجلد. المجلد config: يحتوي على ملفات الضبط Configuration كما يدل اسمه، ولا حاجة للاهتمام بهذا الضبط في هذه السلسلة من المقالات. المجلد database: يحتوي على ملفات التهجير migrations والمصانع factories والبذور seeds، حيث تصف ملفات التهجير بنية قاعدة البيانات، وتُعَد ملفات المصانع والبذور طريقتين مختلفتين يمكننا من خلالهما ملء قاعدة البيانات ببيانات وهمية في لارافيل. المجلد public: يحتوي على الملف index.php الذي يُعَد نقطة الدخول إلى لتطبيق. المجلد resources: يحتوي على ملفات العرض views التي تمثّل جزء الواجهة الأمامية من تطبيق لارافيل. المجلد routes: يحتوي على جميع الموجّهات Routers لعناوين URL للمشروع فعندما ترسل طلب إلى عنوان URL محدد، سيتوجه الطلب إلى المُوجِّه المناسب بناءً على الوجهات التي تحددها الملفات ضمن هذا المجلد. المجلد storage: هو مساحة تخزين المشروع بأكمله، ويحتوي على السجلات logs والعروض المُصرَّفة Compiled Views بالإضافة إلى الملفات التي رفعها المستخدم. المجلد tests: يحتوي على ملفات الاختبار، حيث يُعَد الاختبار مفهومًا أكثر تقدمًا نسبيًا في لارافيل، لذلك لن نوضّحه في هذه السلسلة من المقالات، ولكن يمكنك الاطلاع على مقال كيفية استخدام PHPUnit لاختبار تطبيقات لارافيل لمزيد من التفاصيل حول الاختبارات. المجلد vendor: يتضمن جميع الاعتماديات Dependencies. متغيرات البيئة سنلقي نظرة أيضًا على متغيرات البيئة التي تُخزَّن في ملف .env، حيث أُعِدت الكثير من عمليات الضبط افتراضيًا عندما أنشأنا مشروعنا باستخدام أداة Sail في لارافيل، ولكننا سنتحدث عن عملها فيما يلي. المتغير APP_URL يحدّد المتغير APP_URL عنوان URL للتطبيق، ويكون هذا العنوان هو http://localhost افتراضيًا، ولكن قد تحتاج إلى تغييره إلى العنوان http://127.0.0.1 عندما يعطي العنوانُ http://127.0.0.1 صفحةَ خادم Apache2 الافتراضية. APP_URL=http://127.0.0.1 أو: APP_URL=http://localhost قاعدة البيانات ستثبّت أداة Sail من لارافيل نظامَ إدارة قواعد البيانات MySQL بوصفه التطبيق الخاص بقاعدة البيانات، وتُعرَّف اتصالات قاعدة البيانات كما يلي: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=curl_demo DB_USERNAME=sail DB_PASSWORD=password يمكنك أيضًا تعريف متغيرات بيئة خاصة بك إن احتجت إليها كما يلي، ثم يمكنك الوصول إليها في أي مكان من المشروع: CUSTOM_VARIABLE=true ويمكن الوصول إلى هذا المتغير كما يلي: env('CUSTOM_VARIABLE', true) يمثّل المعامل الثاني القيمة الافتراضية لهذا المتغير، حيث إن لم يكن المتغير CUSTOM_VARIABLE موجودًا، فسيُعاد المعامل الثاني. أساسيات التوجيه Routing في لارافيل سنلقي نظرة في هذا القسم على وِجهات Route لارافيل وبرمجياتها الوسيطة، حيث تُعرَّف الوِجهات في المجلد routes. لاحظ وجود أربعة ملفات مختلفة في المجلد routes افتراضيًا، ولكن ما يهمنا الملفان api.php و web.php بالنسبة لمعظم المشاريع. إذا أردت استخدام لارافيل بطريقة صارمة مع الواجهة الخلفية (بدون العرض)، فيجب أن تحدّد الوِجهات في الملف api.php، ولكننا سنستخدم في هذا المقال لارافيل بوصفه إطار عمل متكامل full-stack، لذلك سنستخدم الملف web.php بدلًا من ذلك. الفرق بين هذين الملفين هو أن الملف api.php تغلِّفه مجموعة برمجيات api الوسيطة وأن الملف web.php موجود ضمن مجموعة برمجيات web الوسيطة، ويوفر هذان الملفان دوالًا مختلفة، وسيكون للوِجهات المحددة في الملف api.php بادئة عنوان URL التي هي /api/، وبالتالي يجب أن يكون عنوان URL مثل العنوان: http://example.com/api/somthing-else للوصول إلى وِجهة api. تقبل الوِجهة الأساسية في لارافيل عنوان URL ثم تعيد قيمة، حيث يمكن أن تكون هذه القيمة سلسلة نصية أو عرضًا أو متحكمًا. انتقل إلى الملف routes/web.php، وسترى أن هناك وِجهة مُعرَّفة مسبقًا هي: routes/web.php. يخبرنا الجزء التالي من الشيفرة البرمجية أنه إذا استقبل لارافيل الوِجهة "/"، فسيعيد عرضًا بالاسم "welcome"، والذي يقع في resources/views/welcome.blade.php: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); افتح المتصفح، وانتقل إلى العنوان http://127.0.0.1:8000، وستحصل على الصفحة التالية: يمكن التحقق من أن welcome.blade.php هو العرض الظاهر من خلال إجراء بعض التغييرات على الملف، ثم تحديث متصفحك للتأكد من تغيير الصفحة. توابع الموجهات لنلقِ الآن نظرة على الموجِّه التالي في routes/web.php ونفهم كيفية عمله: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); نستورد أولًا الصنف Route ونستدعي التابع get()، حيث يتطابق هذا التابع مع تابع HTTP الذي هو GET، وهناك توابع أخرى مُضمَّنة في الصنف Route، حيث تتطابق هذه التوابع مع توابع طلبات HTTP الأخرى كما يلي: Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback); إذا أردتَ أن تطابق الوِجهة بين توابع HTTP متعددة، فيمكنك استخدام تابع match أو any بدلًا من ذلك، حيث يتطلب التابع match() تحديد مصفوفة توابع HTTP التي ترغب في مطابقتها، ويطابق التابع any() ببساطة بين جميع طلبات HTTP. Route::match(['get', 'post'], '/', function () { . . . }); Route::any('/', function () { . . . }); تمرير البيانات إلى العرض يحتوي التابع get() على معاملَين هما: المعامل الأول هو عنوان URL الذي يُفترَض أن يطابقه الموجِّه، والمعامل الثاني هو دالة رد النداء Callback التي تُنفَّذ عند نجاح المطابقة. تُعاد الدالة view() المُضمَّنة ضمن دالة رد النداء، حيث ستبحث هذه الدالة عن ملف العرض المقابل بناءً على المعامل المُمرَّر إليها. يقدم لارافيل اختصارًا بسيطًا هو Route::view() إذا أردتَ إعادة عرض فقط، حيث يسمح لك هذا التابع بتجنب كتابة وِجهة كاملة كما يلي: Route::view('/welcome', 'welcome'); الوسيط الأول لهذا التابع هو عنوان URL، والمعامل الثاني هو العرض المقابل، ويوجد أيضًا وسيط ثالث يسمح بتمرير بعض البيانات إلى هذا العرض كما يلي: Route::view('/welcome', 'welcome', ['name' => 'Taylor']); سنتحدث عن كيفية الوصول إلى البيانات عندما نصل إلى قوالب Blade. من الموجه إلى المتحكم يمكن أن نجعل الموجّه يؤشّر إلى المتحكم الذي يؤشّر بدوره إلى العرض، فالمتحكم هو أساسًا نسخة موسعة من دالة رد النداء، حيث سنتحدث عن المتحكمات بالتفصيل في المقال التالي. يخبرنا سطر الشيفرة البرمجية التالي أنه إذا استقبل الموجّه الوِجهة "/user"، فسيذهب لارافيل إلى الصنف UserController، ويستدعي التابع index: Route::get('/user', [UserController::class, 'index']); معاملات الوجهة تحتاج في بعض الأحيان إلى استخدام أجزاء من عنوان URL بوصفها معاملات، فمثلًا لنفترض أن لدينا مدونة مطورة بالكامل، ويوجد مستخدم يبحث عن منشور في المدونة له الاسم Slug الموجود في الرابط this-is-a-post، ويحاول العثور على هذا المنشور من خلال كتابة http://www.example.com/posts/this-is-a-post في متصفحه. يمكن التأكد من أن المستخدم قد عثر على المنشور الصحيح من خلال أخذ الجزء الموجود بعد posts/ كمعامل وإرساله إلى الواجهة الخلفية، ثم يمكن للمتحكم استخدام هذا المعامل للعثور على المنشور الصحيح وإعادته إلى المستخدم، حيث يمكنك تطبيق ذلك من خلال كتابة الشيفرة البرمجية التالية: Route::get('post/{slug}', [PostController::class, 'show']); سيتأكد السطر السابق من أن موجّه لارافيل يطابق الكلمة الموجودة بعد post/ كمعامل، ويسندها إلى المتغير slug عندما يرسلها لارافيل إلى الواجهة الخلفية. تُضمَّن معاملات الصنف Route دائمًا بين قوسين {} ويجب أن تتكون من أحرف أبجدية، ولا يجوز أن تحتوي على المحرف -. بما أننا لم نذكر المتحكمات في مثالنا، فيمكننا وضع دالة رد نداء بسيطة مكان المعامل الثاني واختبار الشيفرة البرمجية التالية: Route::get('/post/{slug}', function ($slug) { return $slug; }); افتح الآن متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/posts/this-is-a-slug، وستظهر الصفحة التالية: يمكن أيضًا مطابقة معاملات متعددة في عنوان URL كما يلي: Route::get('category/{category}/post/{slug}', [PostController::class, 'show']); سيُسنَد الجزء الموجود بعد category/ إلى المتغير category في هذه الحالة، وسيُسنَد الجزء الموجود بعد post/ إلى المتغير slug. لا تعرف في بعض الأحيان ما إذا كان المعامل موجودًا في عنوان URL، وبالتالي يمكنك جعل هذا المعامل اختياريًا من خلال إلحاقه بإشارة استفهام (?) كما يلي: Route::get('post/{slug?}', [PostController::class, 'show']); وأخيرًا، يمكنك التحقق من صحة المعاملات باستخدام التعابير النمطية Regular Expressions، فمثلًا يمكنك التأكد من أن معرّف المستخدم هو عدد دائمًا كما يلي: Route::get('user/{id}', [UserController::class, 'show'])->where('id', '[0-9]+'); الموجهات المسماة تسمح الوِجهات المُسماة بإنشاء عناوين URL أو إعادة التوجيه بصورة مناسبة إلى وِجهات محددة، حيث يمكنك تحديد اسمٍ لوِجهة من خلال إضافة التابع name إلى سلسلة تعريف الوِجهة كما يلي: Route::get('user/profile', [UserController::class, 'show'])->name('profile'); وبالتالي إذا احتجت إلى الوصول إلى عنوان URL هذا، فيجب أن تستدعي الدالة route('profile') فقط. تجميع الموجهات إذا أردت إنشاء موقع ويب كبير، فيجب أن يكون لديك عشرات أو حتى مئات من الموجّهات، وبالتالي من المنطقي أن تجمّعها مع بعضها بعضًا، فمثلًا يمكنك تجميعها بناءً على البرمجيات الوسيطة middleware كما يلي: Route::middleware(['auth'])->group(function () { Route::get('/user/profile', [UserController::class, 'show']); Route::get('/user/setting', [UserController::class, 'setting']); }); وستُسنَد الآن البرمجية الوسيطة auth إلى الموجّهَين. يمكنك أيضًا إسناد بادئات Prefixes إلى مجموعة من الموجّهات كما يلي: Route::prefix('admin')->group(function () { Route::get('/users', [UserController::class, 'show']); . . . }); سيكون لجميع الموجّهات المحدّدة في هذه المجموعة البادئة /admin/. البرمجيات الوسيطة Middleware البرمجية الوسيطة هي شيء يمكنه فحص وترشيح طلبات HTTP الواردة قبل أن تصل إلى تطبيقك، ويحدث بعد أن تطابق الوِجهة عنوان URL دون تنفيذ دالة رد النداء، وبالتالي هي شيء موجود في منطقة وسطى، ومن هنا جاءت تسميتها بالبرمجيات الوسيطة. يُعَد استيثاق المستخدم User Authentication من أمثلة البرمجيات الوسيطة، حيث إذا كان المستخدم مُستوثَقًا، فستأخذ الوِجهة هذا المستخدم إلى الهدف المفترض، وإن لم يكن الأمر كذلك، فستأخذ المستخدم إلى صفحة تسجيل الدخول أولًا. لن نكتب أيّ برمجيات وسيطة في هذه السلسلة من المقالات، ولكن البرمجية الوسيطة الوحيدة التي نحتاج إلى استخدامها هي البرمجية الوسيطة auth المُضمَّنة لأغراض الاستيثاق، والتي سنشرح أساسياتها. ننشئ برمجية وسيطة من خلال تشغيل الأمر التالي: php artisan make:middleware EnsureTokenIsValid مما سيؤدي إلى إنشاء صنف EnsureTokenIsValid جديد ضمن المجلد app/Http/Middleware. <?php namespace App\Http\Middleware; use Closure; class EnsureTokenIsValid { /** * معالجة الطلب الوارد * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($request->input('token') !== 'my-secret-token') { return redirect('home'); } return $next($request); } } ستحصل هذه البرمجيات الوسيطة على قيمة المفتاح Token من الطلب، وتقارنها مع المفتاح السري المُخزَّن على موقعنا، حيث إذا كان مطابقًا، فانتقل إلى الخطوة التالية، وإذا لم يكن كذلك، فأعِد التوجيه إلى الصفحة الرئيسية. يمكنك استخدام هذه البرمجية الوسيطة من خلال تسجيلها في لارافيل، لذا انتقل إلى الملف app/Http/Kernel.php، وابحث عن الخاصية $routeMiddleware، وأدرِج البرمجية الوسيطة التي أنشأناها. /** * البرمجيات الوسيطة لوِجهات التطبيق * * يمكن إسناد هذه البرمجيات الوسيطة إلى مجموعات أو استخدامها بصورة فردية * * @var array<string, class-string|string> **/ protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; وأخيرًا، تعرّفنا سابقًا على كيفية استخدام البرمجيات الوسيطة مع الموجّه كما يلي: Route::get('/profile', function () {. . .})->middleware('auth'); ترجمة -وبتصرُّف- للمقال Laravel for Beginners #1 - Getting Started لصاحبه Eric Hu. اقرأ أيضًا تعرف على إطار عمل تطوير الويب لارافيل Laravel مقارنة بين Django و Laravel و Rails تثبيت لارافيل وإعداده على Windows وUbuntu إنشاء مدونة باستخدام Laravel 5
-
ناقشنا في مقالٍ سابق أن الرسوميات الحاسوبية ثلاثية الأبعاد تتكون من عمليتين: أولهما إنشاء عالم خيالي داخل الحاسوب، وثانيهما إنتاج صور ثنائية الأبعاد لذلك العالم من مجالات رؤية مختلفة؛ إذ يُعَد إنتاج صورة ثنائية الأبعاد من صورة ثلاثية الأبعاد مثالًا عن عملية الإسقاط Projection، وسنناقش في هذا المقال المفاهيم الأساسية فقط للإسقاط. سنوضّح في هذا المقال المواضيع التالية: تشكيل مستوٍ Plane واحد من شعاعين. مفهوم الإسقاط. إسقاط شعاعٍ على شعاع آخر. صيغة لإسقاط الأشعة. طريقة إسقاط الأشعة خطوةً بخطوة. المكون العمودي لإسقاط الأشعة. مثال لإسقاط الأشعة ثنائية الأبعاد. مثال لإسقاط الأشعة ثلاثية الأبعاد. المساقط ثنائية الأبعاد المتعددة يمكن إنتاج عدد لا نهائي من الصور ثنائية الأبعاد من نموذج واحد ثلاثي الأبعاد، فمثلًا، يعرض الشكل التالي إسقاط نموذج ثلاثي الأبعاد لصندوق على عرضٍ ثنائي الأبعاد؛ إذ تنتِج المواقع المختلفة لمجال الرؤية صورًا مختلفة ثنائية الأبعاد، وتُسقَط الأشعة ثلاثية الأبعاد للنموذج على سطح العرض لتكوين أشعة الصورة ثنائية الأبعاد. تشكيل مستوٍ واحد من شعاعين يمكن تشكيل مستوٍ واحد باستخدام شعاعين ونقطة دائمًا، إلا في حال كان هذان الشعاعان متوازيان. لا بد أنك تتذكر من مادة الهندسة في المرحلة الثانوية أن الخطين غير المتوازيين (ليسا على استقامة واحدة) يحددان مستوٍ واحد، وبالتالي ينطبق الشيء نفسه على الأشعة. تبدأ معظم الأمثلة في هذا المقال بشعاعين ثنائي الأبعاد، ولكن ستكون النتائج صالحة لشعاعين ثلاثي الأبعاد؛ إذ يحدّد شعاعان ثلاثي الأبعاد مستوٍ واحد أيضًا. يوضّح الشكل السابق الشعاع w والشعاع v، وجعلنا ذيل هذين الشعاعين يبدأ من النقطة نفسها لتسهيل التصور، ولكن لا تنسَ أن الأشعة ليس لها موضع محدّد، وجعلنا الشعاع v أفقيًا للسهولة أيضًا، ولكن يمكن أن يكون له أيّ منحى Orientation. إسقاط شعاع على شعاع آخر إذا ضربنا الشعاع v بعدد حقيقي Scalar (أو مقدار سلمي) k، فسيؤدي ذلك إلى تغيير طول هذا الشعاع، ولكنه لن يؤثر على منحاه، وبالتالي ليكن لدينا الشعاعان w و v كما في الشكل التالي: يمكن أن يكون الشعاعان w و v أيّ شعاعين، ونريد شعاعًا u عموديًا على الشعاع v وعددًا حقيقيًا k مضروبًا به، بحيث: w = k v + u ويسمى عندها k v مسقط الشعاع w على الشعاع v. صيغة لكيفية إسقاط الأشعة ملاحظة: إذا كان الشعاعان u و v متعامدان، فإن الجداء النقطي لهما هو: .u · v = 0 يجب أن تتحقق العلاقة: k v + u = w بشرط أن يكون u عموديًا مع v، حتى يكون k v مسقط الشعاع w على الشعاع v؛ إذ نعلم من علم المثلثات أن: طول المسقط k v = | w | cos θ. منحى k v هو شعاع الوحدة vu. تذكر أن أشعة الوحدة Unit Vectors تُستخدَم لتمثيل المنحى في الفضاء ثلاثي الأبعاد، وبالتالي يكون منحى الشعاع v هو شعاع الوحدة vu؛ إذ يبدو المنحى أفقيًا في الشكل السابق، ولكننا استخدمنا ذلك للسهولة فقط. وبما أن cos θ يساوي wu · vu، فسينتج ما يلي: k v = | w | (wu · vu) vu يمثل الجزء | w | (wu · vu) عددًا حقيقيًا بحيث يُضبَط الشعاع v على الطول المطلوب، وهو طول الخط الأفقي باللون الأزرق الفاتح في الشكل السابق؛ بينما يعبّر الجزء المتبقي من الصيغة: vu عن منحى الشعاع v نفسه. طريقة إسقاط الأشعة خطوة بخطوة تذكير: يمكنك حساب شعاع الوحدة من خلال تقسيم الشعاع على طوله. لنتّبع طريقة خطوة بخطوة لإسقاط الشعاع w على v، بدلًا من حفظ الصيغة السابقة لتطبيق عملية الإسقاط. أولًا، نحسب أطوال الأشعة كما يلي: | w | = √(w · w) | v | = √(v · v) ثانيًا، نحسب أشعة الوحدة: wu = w / | w | vu = | v | / | v | ثالثًا، نحسب جيب تمام Cosine الزاوية بين الشعاعين: wu · vu رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu المكون العمودي لإسقاط الأشعة ملاحظة: لا يُعَد مسقط الشعاع w على الشعاع v مماثلًا لمسقط الشعاع v على الشعاع w، إذ يكون منحى الشعاع الناتج عن عملية الإسقاط بالاتجاه نفسه للشعاع المُسقَط عليه. أوجدنا في الفقرة السابقة k v، لذا من السهل العثور على الشعاع المتعامد معه u: w = k v + u، إذًا u = w - k v تعبّر إشارة "+" في الصيغة السابقة عن جمع الأشعة، وتعبّر إشارة "-" عن طرح الأشعة. تدريبات عملية كانت الأشعة في هذا المقال جميعها أشعةً هندسيةً حتى الآن، إذ لم يكن هناك أيّ ذكرٍ للإطارات الإحداثية أو المصفوفات العمودية، ولكن ستنجح هذه النتائج عند تمثيل الأشعة بالمصفوفات العمودية كما سنرى في الأمثلة التالية. تدريب 1: نمثّل الشعاع w بالمصفوفة العمودية (6, 5)T، ونمثّل الشعاع v بالمصفوفة العمودية (9,0)T، ولنوجد مسقط الشعاع w على الشعاع v، أي لنبحث عن الشعاعين k v و u. يمكنك ببساطة قراءة الإجابة من الرسم البياني الورقي التالي، ولكن تظاهر أنك لم تلاحظ ذلك. أولًا، نحسب الأطوال كما يلي: | w | = √ ((6, 5)T·(6, 5)T) = 7.81 | v | = √ ((9, 0)T·(9, 0)T) = 9 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (6, 5)T / 7.81 vu = (9, 0)T / 9 = (1, 0)T ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين وهو: wu · vu = (1/7.81) (6, 5)(1, 0)T · (1, 0)(1, 0)T = 6/7.81 رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu k v = 7.81 (6/7.81) (1, 0)T = 6(1, 0)T = (6, 0)T خامسًا، نحسب الشعاع العمودي: u = w - k v u = (6, 5)T - (6, 0)T = (0, 5)T والنتيجة هي أن u = (6,0)T + (0,5)T كما هو متوقع. وكان هذا المثال سهلًا طبعًا. ملاحظة: ليس عليك أن تحسب طول w، لأنه يُلغَى قبل الإجابة النهائية، لذا فكل ما تحتاج إليه هو مربع طول v، أي |v|2؛ إذ تعرض بعض الكتب صيغًا مختلفة للإسقاط تستفيد من هذه الحقائق. تدريب 2 إليك مثال آخر، ولكنه ليس بهذه السهولة، إذ نمثّل الشعاع w هنا بالمصفوفة العمودية (3.2, 7)T، ونمثّل الشعاع v بالمصفوفة العمودية (8, 4)T؛ ولنوجِد الآن الشعاعين k v و u. أولًا، نحسب الأطوال كما يلي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) | v |2 = (8, 4)T ·(8, 4)T = 80 ثانيًا، نحسب أشعة الوحدة وهي: wu = (3.2, 7)T / | w | vu = (8, 4)T / | v | ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: wu · vu = (3.2, 7)T / | w | · (8, 4)T / | v | = 53.6 /( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu k v = | w | [53.6 / (| w | | v |)] (8, 4)T / | v | k v = 53.6 / (| v |) (8, 4)T / | v | k v = 53.6 / (| v |2) (8, 4)T k v = 53.6 / 80 (8, 4)T k v = ((53.6*8)/80, (53.6*4)/80)T = ( 5.3, 2.68)T خامسًا، نحسب الشعاع العمودي: u = w - k v u = (3.2, 7)T - (5.3, 2.68)T = (-2.1, 4.32)T لنتحقّق الآن من أن: w = k v + u من خلال تعويض النتائج في العلاقة k v + u كما يلي: ( 5.3, 2.68)T + (-2.1, 4.32)T = (3.2, 7.0)T = w فوائد عملية إسقاط الأشعة يجب أن تفهم جيدًا ما يحدث عند إسقاط شعاع على شعاع آخر، إذ تحدث عملية الإسقاط -كما ذكرنا سابقًا- باستمرار عندما يعرض الحاسوب الرسوميات. تُنفَّذ معظم العمليات الحسابية في عتاد الرسوميات الخاصة بالحاسوب، ويجري إسقاط ملايين الأشعة في الثانية الواحدة؛ فإذا أردتَ أن تعرف ما يحدث في الرسوميات الحاسوبية، فيجب أن تعرف ما هي عملية الإسقاط. لنوجِد الآن مسقط الشعاع w على الشعاع v في الرسم البياني التالي: حاول إجراء العمليات الحسابية المطلوبة كما فعلنا في الأمثلة السابقة، ولكن يمكن أن تحصل على الإجابات التالية مباشرةً من قراءة الرسم البياني السابق: w = (4, 8.2)T v = (2.8, 1.75)T k v = (5.8, 1.6)T u = (-1.8, 6.4)T تبدو الإجابات السابقة جيدة، ولكن لنتحقّق منها كما يلي: أولًا، هل w = k v + u؟ (5.8, 1.6)T + (-1.8, 6.4)T = (4, 8.0)T = w (حسنًا، تقريبًا) ثانيًا، هل v · u = 0.0؟ (5.8, 1.6)T · (-1.8, 6.4)T = -10.44 + 10.24 = -0.2 = 0.0 (تقريبًا) ملاحظة: لنفكر في جميع الأشعة المحتملة w و v ومسقط الشعاع w على الشعاع v، إذ يمكن أن يكون الشعاع k v أطول من الشعاع v كما في المثال السابق، ولكن لا يمكن أن يكون الشعاع k v أطول من الشعاع w، فأكبر طول ممكن للشعاع k v هو | w | عندما يكون w و v على استقامة واحدة. مثال آخر لعملية الإسقاط لاحظ أن عملية إسقاط الشعاع w على v لا تعتمد على طول v، بل يؤثر اتجاه الشعاع v فقط على النتائج. يكون طول المسقط هو | w | cos θ، حيث θ هي الزاوية بين الشعاعين، وبما أن دالة جيب التمام لا تزيد عن 1 أبدًا، فلن يكون طول المسقط أبدًا أطول من الشعاع المُسقَط. أسقِط الشعاع w على الشعاع v من خلال الاطلاع على الرسم البياني التالي: وستحصل على الإجابات التالية: w = (-5, 5)T v = (5, 1)T k v = (-3.8, -.8)T u = (-1.3, 5.6)T حصلنا على الإجابات السابقة من خلال قراءة الرسم البياني السابق، ولكنها ليست دقيقة بالضرورة، إذًا لنتحقّق منها. أولًا، هل w = k v + u؟ (-3.8, -0.8)T + (-1.3, 5.6)T = (-5.1, 4.8)T = w (تقريبًا) ثانيًا، هل v · u = 0.0؟ (5, 1)T · (-1.3, 5.6)T = -6.5 + 5.6 = -0.9 = 0.0 (ليس تمامًا) ملاحظة: يمكن لمسقط الشعاع w على الشعاع v أن يؤشّر إلى الاتجاه المعاكس للشعاع v كما حدث في المثال السابق. أمثلة لإسقاط الأشعة ثلاثية الأبعاد سنستعرض هنا بعض الأمثلة حول كيفية إسقاط الأشعة ثلاثية الأبعاد المثال الأول يظهر الرسم البياني السابق الشعاعين التاليين: w = (4, 2.5, 2.5)T (الشعاع الأحمر) v = (5, 0, 3.1)T (الشعاع الأخضر) أسقِط الشعاع w على الشعاع v، وتذكّر أن طول الشعاع v لا يؤثر على الإسقاط، بل يؤثر منحاه عليه فقط، لذا أنزِل رأس سهم الشعاع w على الخط المنقط. يمكن بالنظر تقدير مسقط الشعاع w = (4, 2.5, 2.5)T على الشعاع v = (5, 0, 3.1)T بأنه k v = (4, 0, 2.5)T. يتضمن إسقاط الشعاع w على الشعاع v في هذه الحالة "انهيار Collapsing" البعد y للشعاع w. فلنتحقّق الآن مما إذا كانت النتيجة نفسها رياضيًا، إذ يمكن تطبيق الخطوات نفسها في الفضاء ثلاثي الأبعاد 3D. أولًا، نحسب الأطوال وهي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) | v |2 = ((5, 0, 3.1)T · (5, 0, 3.1)T) = 34.61 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (4, 2.5, 2.5)T / | w | vu = (5, 0, 3.1)T / | v | ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: wu · vu = (4, 2.5, 2.5)T / | w | · (5, 0, 3.1)T / | v | = 27.75 / ( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu k v = | w | (27.75 / (| w | | v |)) (5, 0, 3.1)T / | v | k v = 27.75 / (| v |) (5, 0, 3.1)T / | v | k v = 27.75 / (| v |2) (5, 0, 3.1)T k v = (27.75 / 34.61) (5, 0, 3.1)T k v = ((27.75*5)/34.61, 0, (27.75*3.1)/34.61)T k v = ( 4.00, 0, 2.49)T وبالتالي فإن التقدير بالنظر k v = (4, 0, 2.5)T يطابق النتيجة الرياضية (4, 0, 2.5)T، ويمكن الآن بسهولة حساب الشعاع العمودي: u = w - k v u = (4, 2.5, 2.5)T - (4, 0, 2.5)T = (0, 2.5, 0)T أبقِ الشعاع w كما هو في الرسم البياني السابق، وحاول تصوّر إسقاط w على شعاع مختلف v' ينتج عنه المسقط k ،v' الذي يؤدي إلى انهيار البعدين y و z للشعاع w؛ فمثلًا، إذا أسقطنا الشعاع w = (4, 2.5, 2.5)T على الشعاع v' = (1, 0, 0)T، فستكون النتيجة k v = (4, 0, 0)T. المثال الثاني إذا أسقطنا شعاعًا في فضاء ثلاثي الأبعاد على محاور إحداثية، فستحصل على أحد الأبعاد الثلاثة فقط لذلك الشعاع، لذا لنلقِ نظرة على مثال آخر أصعب. لدينا من الشكل السابق الشعاعان w = (4, 2, 3)T و v = (6, 4, 2)T، ونريد إسقاط الشعاع w على الشعاع v. يمكن أن نقدّر بالنظر أنه إذا "أنزلنا" رأس سهم الشعاع w عموديًا على الشعاع v، فستصل إلى الشعاع v عند حوالي 4/10 من طول v، لذا فإن k تساوي 0.4 تقريبًا و k v يساوي: k v = 0.4 (6, 4, 2)T = (2.4, 1.6, 0.8)T ولكن سيتضح معنا أن هذه النتيجة التقديرية خاطئة بصورة فادحة، إذ يُعَد إيجاد الحل بالنظر في المسائل ثلاثية الأبعاد أمرًا صعبًا، إلّا إذا كانت لديك صورة ثلاثية الأبعاد فعلية أمامك. على هذا الأساس، دعنا نتحقق من الإجابة رياضيًا. أولًا، نحسب الأطوال وهي: | w | = (لا داعي لحسابه، لذا أبقيه بالرموز) | v |2 = ((6, 4, 2)T · (6, 4, 2)T) = 56 ثانيًا، نحسب أشعة الوحدة كما يلي: wu = (4, 2, 3)T / | w | vu = (6, 4, 2)T / | v | ثالثًا، نحسب جيب تمام الزاوية بين الشعاعين كما يلي: wu · vu = (4, 2, 3)T · (6, 4, 2)T / | w | | v | = 38 / ( | w | | v |) رابعًا، نعوّض في صيغة الإسقاط التالية: k v = | w | (wu · vu) vu k v = | w | {38 / (| w | | v |)} {(6, 4, 2)T / | v |} k v = {38 / | v |} {(6, 4, 2)T / | v |} k v = {38 / | v |2} (6, 4, 2)T k v = {38/56} (6, 4, 2)T k v = 0.679 (6, 4, 2)T k v = ( 4.07, 2.7, 1.36)T لنحسب الآن الشعاع u كما يلي، حيث u + k v = w (الشعاع u هو الشعاع الأسود المنقط في الرسم البياني السابق): u + k v = w u = w - k v = (4, 2, 3)T - ( 4.07, 2.7, 1.36)T = (-0.07, -0.7, 1.64)T بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية إسقاط شعاع على شعاع آخر في فضاء ثنائي وثلاثي الأبعاد، وسنتعرّف في المقال التالي على عملية الجداء الشعاعي للأشعة. ترجمة -وبتصرُّف- للفصل Projecting one Vector onto Another من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية إيجاد الزاوية بين شعاعين في فضاء ثلاثي الأبعاد 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D
-
تستخدم لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- مجموعةً متنوعة من هياكل البيانات، وتُعدّ الجداول من أكثرها استخدامًا، ولكن يكون لهذه الجداول قيود أو محدوديات معينة، فمثلًا لا يمكنك تقييد المستخدمين للوصول إلى جزء من الجدول فقط، إذ يجب أن يتمكّن المستخدم من الوصول إلى الجدول بأكمله، وليس إلى بعض الأعمدة ضمنه فقط. لنفترض مثلًا أنك تريد دمج البيانات من عدة جداول في هيكل بيانات جديد، ولكنك لا تريد حذف الجداول الأصلية، حيث يمكنك إنشاء جدول آخر فقط، ولكن سيكون لديك بيانات زائدة مخزَّنة في أماكن متعددة لاحقًا. قد يسبّب ذلك الكثير من الإزعاج، حيث إذا تغيرت بعض بياناتك، فيجب عليك أن تحدّثها في أماكن متعددة، لذا يمكن أن تكون العروض Views مفيدة في مثل هذه الحالات. يُعَد العرض view في لغة SQL جدولًا افتراضيًا، وتكون محتوياته نتيجة لاستعلام محدّد لجدول واحد أو أكثر، حيث تُعرَف هذه الجداول باسم الجداول الأساسية Base Tables. يقدّم هذا المقال نظرة عامة حول عروض SQL وفوائدها، ويوضّح كيفية إنشاء العروض والاستعلام عنها وتعديلها وتدميرها باستخدام صيغة SQL المعيارية. مستلزمات العمل يجب أن يكون لديك حاسوب يشغّل أحد أنواع أنظمة إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- التي تستخدم لغة SQL. اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، كما يمكنك الاطلاع على مقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة. نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا الخطوات باستخدام مستخدم مُنشَأ وفق الطريقة الموضحة في الخطوة 3 من المقال. ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. ستحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لتتمكن من التدرب على إنشاء العروض والتعامل معها، لذا اطّلع على القسم التالي للحصول على تفاصيل حول كيفية الاتصال بخادم MySQL وإنشاء قاعدة بيانات الاختبار المُستخدَمة في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية إذا كان نظام قاعدة بيانات SQL الخاص بك يعمل على خادم بعيد، فاتصل بالخادم مُستخدمًا بروتوكول SSH من جهازك المحلي كما يلي: $ ssh user@your_server_ip ثم افتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بك مكان user: $ mysql -u user -p أنشِئ قاعدة بيانات باسم views_db في موجّه الأوامر: mysql> CREATE DATABASE views_db; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر خرج كما يلي: الخرج Query OK, 1 row affected (0.01 sec) يمكنك اختيار قاعدة البيانات views_db من خلال تنفيذ تعليمة USE التالية: mysql> USE views_db; ويكون الخرج كما يلي: الخرج Database changed اخترنا قاعدة بيانات views_db، وسننشئ جدولين ضمنها. لنفترض مثلًا أنك تدير خدمة رعاية للكلاب، فقررت استخدام قاعدة بيانات SQL لتخزين معلومات حول كل كلب مُسجَّل في الخدمة، بالإضافة إلى معلومات كل متخصص في رعاية الكلاب توظفه خدمتك، حيث نظّمت هذه المعلومات من خلال إنشاء جدولين: أحدهما يمثل الموظفين والآخر يمثل الكلاب التي تعتني بها خدمتك. سيحتوي الجدول الذي يمثل موظفيك على الأعمدة التالية: emp_id: رقم تعريف لكل موظف يقدّم رعايةً للكلاب، ونعبّر عنه باستخدام نوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي Primary Key للجدول، أي أن كل قيمة ستمثّل معرّفًا فريدًا للصف الخاص بها، وسيكون لهذا العمود أيضًا قيد UNIQUE مطبَّق عليه، إذ يجب أن تكون كل قيمة في المفتاح الرئيسي فريدة. emp_name: اسم الموظف، ونعبّر عنه باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا. نفّذ تعليمة CREATE TABLE التالية لإنشاء جدول بالاسم employees ويحتوي على العمودين التاليين: mysql> CREATE TABLE employees ( mysql> emp_id int UNIQUE, mysql> emp_name varchar(20), mysql> PRIMARY KEY (emp_id) mysql> ); سيحتوي الجدول الآخر الذي يمثل الكلاب على الأعمدة الستة التالية: dog_id: رقم تعريف لكل كلب ويُعبَّر عنه بنوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي للجدول dogs مثل العمود emp_id في الجدول employees. dog_name: اسم الكلب ويُعبَّر عنه باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا. walker: يخزّن هذا العمود رقم معرّف الموظف الذي يرعى كل كلب. walk_distance: المسافة التي يجب أن يمشيها كل كلب عند اصطحابه للتمرين، ويُعبَّر عنه باستخدام نوع البيانات decimal، ويمكن أن تحتوي القيم في هذا العمود على ثلاثة أرقام على الأكثر مع وجود رقمين من هذه الأرقام على يمين الفاصلة العشرية. meals_perday: توفر هذه الخدمة لكل كلب عددًا معينًا من الوجبات كل يوم، حيث يحتوي هذا العمود على عدد الوجبات التي يجب أن يحصل عليها كل كلب يوميًا حسب طلب مالكه، ويستخدم نوع البيانات int فهو عدد صحيح. cups_permeal: يمثل هذا العمود عدد أكواب الطعام التي يجب أن يحصل عليها كل كلب في كل وجبة، ويُعبَّر عنه بنوع البيانات decimal مثل العمود walk_distance، ويمكن أن تحتوي القيم في هذا العمود على ما يصل إلى ثلاثة أرقام مع وجود رقمين من هذه الأرقام على يمين الفاصلة العشرية. نتأكد من أن العمود walker يحتوي على القيم التي تمثل أرقام معرّف الموظف الصالحة فقط من خلال تطبيق قيد مفتاح خارجي Foreign Key على العمود walker الذي يشير إلى العمود emp_ID الخاص بالجدول employees. يُعَد قيد المفتاح الخارجي طريقة للتعبير عن العلاقة بين جدولين من خلال اشتراط أن تكون القيم الموجودة في العمود الذي طُبِّق المفتاح الخارجي عليه موجودة في العمود الذي يشير إليه، حيث يشترط قيد FOREIGN KEY في المثال التالي أن تكون أيّ قيمة مضافة إلى العمود walker في الجدول dogs موجودةً في العمود emp_ID الخاص بالجدول employees. لننشئ جدولًا بالاسم dogs يحتوي على هذه الأعمدة باستخدام الأمر التالي: mysql> CREATE TABLE dogs ( mysql> dog_id int UNIQUE, mysql> dog_name varchar(20), mysql> walker int, mysql> walk_distance decimal(3,2), mysql> meals_perday int, mysql> cups_permeal decimal(3,2), mysql> PRIMARY KEY (dog_id), mysql> FOREIGN KEY (walker) mysql> REFERENCES employees(emp_ID) ); يمكنك الآن تحميل الجدولين ببعض البيانات التجريبية النموذجية. نفّذ عملية الإدخال INSERT INTO التالية لإضافة ثلاثة صفوف من البيانات تمثّل ثلاثة من موظفي الخدمة إلى الجدول employees: mysql> INSERT INTO employees mysql> VALUES mysql> (1, 'Peter'), mysql> (2, 'Paul'), mysql> (3, 'Mary'); ثم نفّذ العملية التالية لإدخال سبعة صفوف من البيانات في الجدول dogs: mysql> INSERT INTO dogs mysql> VALUES mysql> (1, 'Dottie', 1, 5, 3, 1), mysql> (2, 'Bronx', 3, 6.5, 3, 1.25), mysql> (3, 'Harlem', 3, 1.25, 2, 0.25), mysql> (4, 'Link', 2, 2.75, 2, 0.75), mysql> (5, 'Otto', 1, 4.5, 3, 2), mysql> (6, 'Juno', 1, 4.5, 3, 2), mysql> (7, 'Zephyr', 3, 3, 2, 1.5); وأصبحتَ الآن جاهزًا لمتابعة بقية هذا المقال والبدء في تعلّم كيفية استخدام العروض في لغة SQL. فهم وإنشاء العروض Views يمكن أن تصبح استعلامات SQL معقدة، ولكن إحدى الفوائد الرئيسية للغة SQL هي أنها تتضمن العديد من الخيارات والتعليمات التي تسمح لك بترشيح بياناتك بمستوى عالٍ من الدقة والتحديد. إذا كانت لديك استعلامات معقدة تريد تشغيلها بصورة متكررة، فقد يصبح الاضطرار إلى كتابتها باستمرار أمرًا مملًا، وإحدى الطرق لحل هذه المشكلات هي استخدام العروض. تُعَد العروض views جداولًا افتراضية كما ذكرنا سابقًا، وهذا يعني أنها تشبه الجداول وظيفيًا، ولكنها تمثّل نوعًا مختلفًا من هياكل البيانات لأن العرض لا يحتوي على أيّ بيانات خاصة به، فهو يسحب البيانات من جدول أساسي واحد أو أكثر، وتكون المعلومات الوحيدة حول العرض التي سيخزنها نظام إدارة قواعد البيانات DBMS هي هيكل العرض. تُسمَّى العروض أحيانًا بالاستعلامات المحفوظة Saved Queries، لأنها تمثّل الاستعلامات المحفوظة باسم مُحدَّد لتسهيل الوصول إليها. ليكن لدينا المثال التالي، تخيل أن أعمال رعاية الكلاب الخاصة بك ناجحة وتريد طباعة جدول يومي لجميع موظفيك، إذ يجب أن يحتوي هذا الجدول على كل كلب ترعاه الخدمة، والموظف المكلف برعايته، والمسافة التي يجب أن يمشيها كل كلب يوميًا، وعدد الوجبات التي يجب إطعامها لكل كلب يوميًا، وكمية الطعام التي ينبغي أن يحصل كل كلب في كل وجبة. استخدم مهاراتك في لغة SQL لإنشاء استعلام مع بياناتٍ تجريبية نموذجية من الخطوة السابقة لاسترجاع جميع هذه المعلومات للجدول، ولاحظ أن هذا الاستعلام يتضمن صيغة JOIN لسحب البيانات من الجدولين employees و dogs: mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, cups_permeal mysql> FROM employees JOIN dogs ON emp_ID = walker; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 7 rows in set (0.00 sec) لنفترض أنه يجب تنفيذ هذا الاستعلام بشكل متكرر، إذ قد يصبح أمرًا مملًا إذا اضطررت إلى كتابة الاستعلام مرارًا وتكرارًا، وخاصةً عند إجراء تعليمات استعلام أطول وأكثر تعقيدًا، وإذا أردتَ إجراء تعديلات طفيفة على الاستعلام أو التوسّع فيه، فقد يكون الأمر مملًا عند استكشاف الأخطاء وإصلاحها مع وجود العديد من الاحتمالات للأخطاء الصياغية. يمكن أن يكون العرض مفيدًا في مثل هذه الحالات، لأنه يُعَد جدولًا مشتقًا من نتائج الاستعلام. تستخدم معظم أنظمة RDBMS الصيغة التالية لإنشاء العروض: CREATE VIEW view_name AS SELECT statement; يمكنك بعد تعليمة CREATE VIEW اختيار اسمٍ للعرض الذي ستستخدمه للإشارة إليه لاحقًا، ثم تدخِل الكلمة المفتاحية AS، ثم تضع استعلام SELECT الذي تريد حفظ خرجه. يمكن أن يكون الاستعلام الذي تستخدمه لإنشاء عرضك أيّ تعليمة SELECT صالحة، ويمكن أن تستعلم التعليمة التي تضمّنها عن جدول أساسي واحد أو أكثر طالما أنك تستخدم الصيغة الصحيحة. جرّب إنشاء عرض باستخدام استعلام المثال السابق، حيث تسمّي عملية CREATE VIEW العرض بالاسم walking_schedule: mysql> CREATE VIEW walking_schedule mysql> AS mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, cups_permeal mysql> FROM employees JOIN dogs mysql> ON emp_ID = walker; ستتمكّن بعد ذلك من استخدام هذا العرض والتفاعل معه كما تفعل مع أيّ جدول آخر، فمثلًا يمكنك تنفيذ الاستعلام التالي لإعادة جميع البيانات الموجودة في العرض: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 7 rows in set (0.00 sec) يُعَد هذا العرض مشتقًا من جدولين آخرين، ولكنك لن تتمكّن من الاستعلام عن العرض لأيّ بيانات من هذين الجدولين إن لم تكن موجودة مسبقًا في هذا العرض. يحاول الاستعلام التالي استرجاع العمود walker من العرض walking_schedule، ولكن سيفشل هذا الاستعلام لأن العرض لا يحتوي على أيّ أعمدة بهذا الاسم: mysql> SELECT walker FROM walking_schedule; الخرج ERROR 1054 (42S22): Unknown column 'walker' in 'field list' يعيد هذا الخرج رسالة خطأ لأن العمود walker هو جزء من الجدول dogs، ولكنه غير مُضمَّنٍ في العرض الذي أنشأناه. يمكنك أيضًا تنفيذ الاستعلامات التي تتضمّن دوالًا تجميعية Aggregate Functions تعالج البيانات ضمن العرض، يستخدم المثال التالي الدالة التجميعية MAX مع عبارة GROUP BY للعثور على أطول مسافة يجب على الموظف أن يمشيها في يوم محدّد: mysql> SELECT emp_name, MAX(walk_distance) AS longest_walks mysql> FROM walking_schedule GROUP BY emp_name; الخرج +----------+---------------+ | emp_name | longest_walks | +----------+---------------+ | Peter | 5.00 | | Paul | 2.75 | | Mary | 6.50 | +----------+---------------+ 3 rows in set (0.00 sec) توجد فائدة أخرى للعروض كما ذكرنا سابقًا، وهي أنه يمكنك استخدامها لتقييد وصول مستخدم قاعدة البيانات إلى العرض فقط بدلًا من الوصول إلى الجدول أو قاعدة البيانات بأكملها. لنفترض مثلًا أنك وظّفتَ مدير مكتب لمساعدتك في إدارة الجدول الزمني، وتريد أن يصل إلى معلومات الجدول دون الوصول لأي بيانات أخرى في قاعدة البيانات، حيث يمكنك إنشاء حساب مستخدم جديد له في قاعدة بياناتك كما يلي: mysql> CREATE USER 'office_mgr'@'localhost' IDENTIFIED BY 'password'; يمكنك بعد ذلك منح هذا المستخدم الجديد صلاحية وصول للقراءة إلى العرض walking_schedule فقط باستخدام التعليمة GRANT كما يلي: mysql> GRANT SELECT ON views_db.walking_schedule to 'office_mgr'@'localhost'; وبالتالي سيتمكّن الشخص الذي لديه صلاحية الوصول إلى حساب مستخدم MySQL الذي هو office_mgr من تنفيذ استعلامات SELECT في العرض walking_schedule فقط. تغيير وحذف العروض Views إذا أضفتَ أو غيّرتَ بياناتٍ في أحد الجداول التي نشتق العرض منها، فستُضاف أو تُحدَّث البيانات ذات الصلة في العرض تلقائيًا. نفّذ الأمر INSERT INTO التالي لإضافة صف آخر إلى الجدول dogs: mysql> INSERT INTO dogs VALUES (8, 'Charlie', 2, 3.5, 3, 1); يمكنك بعد ذلك استرجاع جميع البيانات من العرض walking_schedule مرة أخرى كما يلي: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | cups_permeal | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 1.00 | | Peter | Otto | 4.50 | 3 | 2.00 | | Peter | Juno | 4.50 | 3 | 2.00 | | Paul | Link | 2.75 | 2 | 0.75 | | Paul | Charlie | 3.50 | 3 | 1.00 | | Mary | Bronx | 6.50 | 3 | 1.25 | | Mary | Harlem | 1.25 | 2 | 0.25 | | Mary | Zephyr | 3.00 | 2 | 1.50 | +----------+----------+---------------+--------------+--------------+ 8 rows in set (0.00 sec) لاحظ وجود صف آخر في مجموعة نتائج الاستعلام، والذي يمثّل البيانات التي أضفتها إلى الجدول dogs، ولكن لا يزال العرض يسحب البيانات ذاتها من الجداول الأساسية نفسها، لذلك لم تغيّر هذه العملية العرض. تسمح لك العديد من أنظمة RDBMS بتحديث هيكل العرض بعد إنشائه باستخدام صيغة CREATE OR REPLACE VIEW: mysql> CREATE OR REPLACE VIEW view_name mysql> AS mysql> new SELECT statement إذا كان العرض الذي اسمه view_name موجودًا مسبقًا في هذه الصيغة، فسيحدّث نظام قاعدة البيانات هذا العرض بحيث يمثّل البيانات التي تعيدها التعليمة new SELECT statement. إذا لم يكن العرض بهذا الاسم موجودًا، فسينشئ نظام إدارة قواعد البيانات DBMS عرضًا جديدًا. لنفترض أنك تريد تغيير العرض walking_schedule ليسرد إجمالي كمية الطعام التي تناولها كل كلب على مدار اليوم بدلًا من سرد عدد أكواب الطعام التي يتناولها كل كلب في كل وجبة، ويمكنك تغيير العرض باستخدام الأمر التالي: mysql> CREATE OR REPLACE VIEW walking_schedule mysql> AS mysql> SELECT emp_name, dog_name, walk_distance, meals_perday, (cups_permeal * mysql> meals_perday) AS total_kibble mysql> FROM employees JOIN dogs ON emp_ID = walker; إذا أجربتَ الآن استعلامًا على هذا العرض، فستمثّل مجموعة النتائج بيانات العرض الجديدة كما يلي: mysql> SELECT * FROM walking_schedule; الخرج +----------+----------+---------------+--------------+--------------+ | emp_name | dog_name | walk_distance | meals_perday | total_kibble | +----------+----------+---------------+--------------+--------------+ | Peter | Dottie | 5.00 | 3 | 3.00 | | Peter | Otto | 4.50 | 3 | 6.00 | | Peter | Juno | 4.50 | 3 | 6.00 | | Paul | Link | 2.75 | 2 | 1.50 | | Paul | Charlie | 3.50 | 3 | 3.00 | | Mary | Bronx | 6.50 | 3 | 3.75 | | Mary | Harlem | 1.25 | 2 | 0.50 | | Mary | Zephyr | 3.00 | 2 | 3.00 | +----------+----------+---------------+--------------+--------------+ 8 rows in set (0.00 sec) يمكنك حذف العروض باستخدام صيغة DROP مثل معظم هياكل البيانات الأخرى التي يمكنك إنشاؤها في لغة SQL، وإليك مثالًا: DROP VIEW view_name; يمكنك مثلًا حذف العرض walking_schedule باستخدام الأمر التالي: mysql> DROP VIEW walking_schedule; يؤدي الأمر السابق إلى إزالة العرض walking_schedule من قاعدة بياناتك، ولكنه لن يحذف أيًّا من بيانات قاعدة بياناتك المتعلقة بالعرض إلّا إذا أزلتها من الجداول الأساسية. الخلاصة تعلّمنا في هذا المقال ما هي عروض SQL وكيفية إنشائها والاستعلام عنها وتغييرها وحذفها من قاعدة البيانات، وتعرّفنا على فوائد العروض، وأنشأنا مستخدم MySQL الذي يمكنه فقط قراءة البيانات من العرض التجريبي الذي أنشأناه. يجب أن تعمل الأوامر الواردة في أمثلة هذا المقال على معظم قواعد البيانات العلاقية، ولكن يجب أن تدرك أن كل قاعدة بيانات SQL لها تقديمها الفريد من هذه اللغة، لذا يجب عليك الرجوع إلى التوثيق الرسمي لنظام إدارة قواعد البيانات DBMS الخاص بك للحصول على وصف أشمل لكل أمر ومجموعته الكاملة من الخيارات. ننصحك بالاطلاع على سلسلة تعلم SQL في أكاديمية حسوب للمزيد حول كيفية التعامل مع لغة SQL. ترجمة -وبتصرف- للمقال How To Use Views in SQL لصاحبه Mark Drake. اقرأ أيضًا الاستعلام عن البيانات في SQL جلب الاستعلامات عبر SELECT في SQL المرجع المتقدم إلى لغة SQL لغة معالجة البيانات DML الخاصة بلغة SQL
-
رأينا في المقالات السابقة أن نستطيع استخدام الجداء النقطي Dot Product من أجل الآتي: حساب طول الشعاع: حيث يكون الجداء النقطي للشعاع مع نفسه = الطول2. اكتشاف التعامد بين شعاعين: حيث يكون الجداء النقطي لشعاعين متعامدين = 0. حساب الزاوية بين شعاعين: حيث يكون cosθ = حاصل الجداء النقطي لشعاعي وحدة Unit Vectors. لن نضيف في هذا المقال شيئًا جديدًا عمّا تعلمناه في المقال السابق، ولكننا سنوسّع مفهوم حساب الزاوية بين شعاعين ليشمل حساب الزاوية بين شعاعين في فضاء ثلاثي الأبعاد. سنتعرّف في هذا المقال على المواضيع الآتية: صيغة حساب جيب تمام Cosine الزاوية بين أشعة الوحدة. قائمة بالخطوات التي يجب اتخاذها لتطبيق هذه الصيغة على الأشعة التي ليست أشعة وحدة. استخدام الأشعة في مسائل الهندسة الفراغية. الزوايا في الفضاء ثلاثي الأبعاد تقع أي حافتين من الحواف الثلاثة المشكّلة لزاوية في صندوق من الورق المقوى في مستوٍ واحد، وتساوي الزاوية بينهما 90 درجة، إذ تتقاطع جميع حواف الصندوق بزوايا قائمة، ولكن لن تساوي الزوايا بين الحواف 90 درجة عادةً عند إسقاط الحواف لتكوين صورة ثنائية الأبعاد. الزوايا بين محاور الإحداثيات لا يؤدي تغيير مجال رؤيتك إلى تغيير الكائنات ثلاثية الأبعاد، ولكن ستتغيّر الصورة ثنائية الأبعاد. تعتمد الزاوية بين شعاعين في الفضاء ثلاثي الأبعاد على علاقتهما ببعضهما البعض، ولا تعتمد على مجال رؤيتك أو إطار الإحداثيات الذي تستخدمه؛ ولكن زوايا الصورة تعتمد كثيرًا على مجال الرؤية عندما تعرض صورة ثنائية الأبعاد مشهدًا ثلاثي الأبعاد. رأينا في المقال السابق الصيغة التالية لحساب الزاوية θ بين شعاعي الوحدة: au · bu = cosθ إذًا لنطبّق هذه الصيغة على شعاعي الوحدة الموازيين للمحور x والمحور y وهما: (1,0,0)T و (0,1,0)T لحساب الزاوية المحصورة بين المحورين x و y كما يلي: (1,0,0)T · (0,1,0)T = 0 وبالتالي فإن المحورين x و y متعامدان كما توقعنا. الزاوية بين شعاعين كان المثال السابق سهل التصور والحساب، إذًا لنجرب شيئًا أصعب، حيث يظهر الشكل الآتي شعاعين: الشعاع a الذي تمثله المصفوفة العمودية (4,4,4)T. الشعاع b الذي تمثله المصفوفة العمودية (4,0,4)T. لاحظ أن الزاوية بين الشعاعين لا تساوي 90 درجة، وبالتالي يصعب رؤيتها مقارنةً بالمثال السابق. يمكن أن نخمّن بصريًا إلى حدٍ ما أن الزاوية بين الشعاعين a و b أقل من منتصف المسافة بين المستوي العمودي والأفقي، إذ تكون الزاوية التي تساوي 35 درجة تخمينًا جيدًا، ولكن التخمين غير كافٍ ونحتاج صيغةً لحساب الزاوية، وهذا ما سنوضّحه في الفقرة التالية. صيغة حساب الزاوية بين شعاعين لاحظ أنه عند الانتقال من ذيل الشعاع a إلى رأسه تزيد المسافة العمودية بمقدار 4، بينما تزيد المسافة الأفقية بمقدار 4 √2، وظل الزاوية Tangent هو: 4 / (4 √2) = 1.0/ √2 = 0.7071 وبالتالي فإن الزاوية مع المستوي الأفقي هي: arctan( 0.7071 ) = 35.26 درجة. وبما أن الشعاع b يقع في المستوي الأفقي، فإن الزاوية بين الشعاعين يجب أن تساوي هذه القيمة. وبالتالي فإن صيغة حساب الزاوية θ بين شعاعي وحدة هي: au · bu = cosθ ويمكننا استخدام هذه الصيغة مع الأشعة التي ليست أشعة وحدة باتباع الخطوات التالية: توحيد Normalize كل شعاع. حساب الجداء النقطي لشعاعي الوحدة الناتجين. استخدم arc cos للحصول على الزاوية. لنطبّق هذه الصيغة على الشعاع a الذي تمثله (4,4,4)T والشعاع b الذي تمثله (4,0,4)T كما يلي: | a | = √(16 + 16 + 16) = 4√3 و | b | = √(16 + 16) = 4√2 au = (4, 4, 4)T / (4√3) و au = (4, 0, 4)T / (4 √2 ) au · bu = (16 + 16)/( (4√3)(4 √2) ) = 2/(√3√2 ) = √2 /√3 = cosθ cosθ = 0.81649 وبالتالي فإن الزاوية بين الشعاعين هي: θ = 35.26 درجة. لا تُعَد الحسابات الكثيرة في التمرين السابق هي الغرض الحقيقي، ولكن الهدف هو توضيح الصيغة au · bu = cosθ، والتي تُعَد مهمةً في كل جزء من أجزاء الرسوميات ثلاثية الأبعاد. تدريب عملي يوضح الشكل الآتي شعاعين تمثّلهما المصفوفتان العموديتان التاليتان: f = (2, 4, 6)T g = (6, 4, 3)T يمكن أن ترغب في قياس الزاوية بين الشعاعين من خلال وضع مِنقلة مسطحة بين الشعاعين، ولكن لا يمكنك ذلك لأن كل ما تراه هو إسقاطٌ للشعاعين على الشاشة. يمكننا تخمين الزاوية التي تفصل بين الشعاعين بمقدار 30 درجة، ولكنه مجرد تخمين، إذًا لنطبّق الصيغة لمعرفة مدى صحة هذا التخمين. أولًا، نحسب الأطوال وهي: | f |2 = (2, 4, 6)T · (2, 4, 6)T = 4 + 16 + 36 = 56 | g |2 = (6, 4, 3)T · (6, 4, 3)T = 36 + 16 + 9 = 61 ثانيًا، نوحّد الشعاعين كما يلي: fu = (2, 4, 6)T / √56 gu = (6, 4, 3)T / √61 ثالثًا، نحسب الجداء النقطي لهما وهو: fu · gu = (2, 4, 6)T · (6, 4, 3)T / ( √56 √61) = (12+16+18)/( √56 √61) = 46 / ( √56 √61) = 0.78704 رابعًا، نحسب الزاوية بينهما كما يلي: cos θ = 0.78704 θ = arc cos 0.78704 = 38.1 درجة لاحظ أن الجواب ليس بعيدًا جدًا عن التخمين. الزاوية بين اتجاهي شعاعين ليس من الضروري أن يتلامس الشعاعان عند ذيليهما حتى تكون هناك زاوية بينهما، فليس للأشعة موضع محدد، إذ يمكنك أخذ الجداء النقطي لأي شعاعين ثلاثي الأبعاد أو المصفوفات العمودية التي تمثلها. قد يبدو من الغريب قياس الزاوية بين الأشياء التي ليس لها موضع محدد، ولكن العديد من الظواهر لها منحًى Orientation، دون أن يكون لها موضع محدد؛ إذ قد تأتي الرياح من الغرب في أحد الأيام، ومن الشمال في اليوم التالي (بفارق 90 درجة)؛ وقد يصل ضوء الشمس عند الظهيرة في الصيف بزاوية مختلفة عن زاوية وصوله عند الظهيرة في الشتاء. يستخدم التظليل في الرسوميات ثلاثية الأبعاد أشعةً لمنحى السطح، وأشعة أخرى لاتجاه كل مصدر للضوء. ليكن باسم وبلال جالسين على الشاطئ، ويريدان تسمير بشرتهما كما في الشكل التالي: يؤشر إطار الإحداثيات المناسب إلى المحور x شمالًا، والمحور y للأعلى بصورة مستقيمة والمحور z شرقًا، حيث يكون الاتجاه نحو الشمس في هذا الإطار هو (-3, 4, 0)T / 5، وشعاع الوحدة العمودي على ظهر باسم هو ( -1, 2, 2)T / 3، وشعاع الوحدة العمودي على ظهر بلال هو ( -2, 1, 2)T / 3. إذًا سيكون أحدهما متمتعًا بأفضل موضع للتسمير السريع، حيث سنوضّح ذلك كما يلي: الاتجاه نحو الشمس = ( -3, 4, 0)/5. الاتجاه العمودي على ظهر باسم = ( -1, 2, 2)/3. الاتجاه العمودي على ظهر بلال = ( -2, 1, 2)/3. يجب أن يكون ظهرك مُوجَّهًا مباشرةً نحو الشمس ليصل إليك أكبر قدرٍ من أشعتها، كما يجب أن تكون الزاوية بين الاتجاه العمودي على ظهرك واتجاه الشمس صغرى. بالنسبة لباسم: ( -1, 2, 2)/3 · ( -3, 4, 0)/5 = (3+8+0)/15 = 11/15 arc cos( 11/15 ) = 42.8 درجة وبالنسبة لبلال: ( -2, 1, 2)/3 · ( -3, 4, 0)/5 = (6+4+0)/15 = 10/15 arc cos( 14/15 ) = 48.2 درجة وبالتالي سيحصل باسم على تسمير لبشرته بصورة أسرع. تدريبات عملية تدريب 1: يظهر الرسم البياني الآتي الشعاعين التاليين: q = (-2, 4, 3)T p = (3, 1, -4)T وضعنا ذيل الأشعة عند نقطة الأصل لسهولة التصور، إذ تؤشّر الأشعة إلى ثُمنَين Octants مختلفتين من الفضاء ثلاثي الأبعاد. لنحسب الآن الزاوية بين هذين الشعاعين كما يلي: أولًا، نحسب الأطوال وهي: | p |2 = ( -2, 4, 3)T · ( -2, 4, 3)T = 4 + 16 + 9 = 29 | q |2 = ( 3, 1, -4)T · ( 3, 1, -4)T = 9 + 1 + 16 = 26 ثانيًا، نوحّد الشعاعين كما يلي: pu = (-2, 4, 3)T / √29 qu = (3, 1, -4)T / √26 ثالثًا، نحسب الجداء النقطي لهما وهو: pu · qu = (-2, 4, 3)·(3, 1, -4)T / ( √29 √26) = (-6 + 4 - 12)/( √29 √26) = -14/( √29 √26) = -0.50985 رابعًا، نحسب الزاوية بينهما كما يلي: cos θ = -0.50985 θ = arc cos( -0.50985) = 120.654 درجة تدريب2: أوجد جيب تمام الزاوية بين شعاعي الوحدة التاليين: s = (1, 0, 1)T /√2 t = (1, 1, 1)T /√3 cosθ = (1, 0, 1)T · (1, 1, 1)T / ( √2 √3) = 2 / ( √2 √3) = √2 / √3 = 0.8164 استخدام صيغة الجداء النقطي لحل المسائل الهندسية يمكن استخدام صيغة الجداء النقطي لحل بعض المسائل الهندسية التي قد تكون صعبة الحل، فمثلًا، تتطابق الحواف الحمراء في الشكل التالي على محاور الإحداثيات، وتشترك في نقطة النهاية (0, 0, 0)، وتنتهي الحافة على طول المحور x عند x=2، وتنتهي الحافة على طول المحور y عند y=3، وتنتهي الحافة على طول المحور z عند z=4، وتربط الحواف المتبقية نقاط النهاية هذه. لنفترض الآن أنك تريد حساب الزاوية بين الحافتين الخضراويتين، وقد يكون ذلك مملًا إلى حدٍ ما باستخدام علم المثلثات، لذا يمكنك: تشكيل شعاع لكل حافة خضراء (من خلال طرح نقاط النهاية). توحيد كل شعاع منهما. استخدام قاعدة الجداء النقطي لحساب الزاوية بينهما. إذًا لنطبّق هذه الخطوات لحساب الزاوية بين الحافتين الخضراويتين كما يلي: أولًا، نقاط النهاية هي: (0, 3, 0) إلى (2, 0, 0) (0, 3, 0) إلى (0, 0, 4) ثانيًا، نحسب أشعة الإزاحة وهي: a = (2, 0, 0) - (0, 3, 0) = (2, -3, 0)T b = (0, 0, 4) - (0, 3, 0) = (0, -3, 4)T ثالثًا، نحسب الأطوال كما يلي: | a |2 = (2, -3, 0)T · (2, -3, 0)T = 13 | b |2 = (0, -3, 4)T · (0, -3, 4)T = 25 رابعًا، نوحّد الأشعة كما يلي: au = (2, -3, 0)T / √13 bu = (0, -3, 4)T / 5 خامسًا، نحسب الجداء النقطي لهما: au · bu = (2, -3, 0)T · (0, -3, 4)T / (5 √13) = 9 / (5 √13) = 0.49923 سادسًا، نحسب الزاوية بينهما كما يلي: cosθ = 0.49923 θ = arc cos 0.49923 = 60.051 درجة إذا حسبتَ أشعة الإزاحة بالطريقة المعاكسة (بالطرح من (0, 3, 0))، فلن يؤثر ذلك على الإجابة، إذ يمكن أن يؤشّر شعاعان إلى اتجاهين متعاكسين، ولكن حاصل جدائهما النقطي سيكون نفسه. بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على كيفية حساب الزاوية بين شعاعين في فضاء ثلاثي الأبعاد، وسنتعرّف في المقال التالي على إسقاط شعاعٍ ما على شعاع آخر. ترجمة -وبتصرُّف- للفصل The Angle between 3D Vectors من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية إيجاد الزاوية بين شعاعين في فضاء ثنائي الأبعاد 2D الجداء النقطي وارتباطه بطول الأشعة وتعامدها في التصاميم 3D تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D