مدونة عمار الخوالده

مقدمة إلى إدارة الاعتماديات في PHP باستخدام Composer


مقدمة إلى إدارة الاعتماديات في PHP باستخدام Composer

مقدمة

Composer أداة لإدارة الاعتماديات (Dependencies) يمكن من خلالها تثبيت وتحديث الحزم (Packages) وإدارة نسخها (Versions).

يمكنك تثبيت Composer على جهازك باتباع الخطوات الموجودة في هذا الرابط: (Download Composer)

تثبيت حزمة باستخدام Composer

لنفترض أن لدينا مشروعا فيه ملف واحد index.php، ونحتاج لاستخدام Package جاهزة للتحقق من صحة البريد الالكتروني.

سنستخدم هذه الـ Package كمثال:

https://github.com/egulias/EmailValidator

لتحميل أي Package باستخدام Composer، نُنفذُ الأمر require في مجلد المشروع:

1composer require egulias/email-validator

بعد انتهاء تنفيذ الأمر، تلاحظ أن Composer قام بإنشاء ملفات جديدة:

محتويات مجلد المشروع بعد تنفيذ أمر Composer

إذا قمت بفتح الملف composer.json ستجد محتواه مشابها لما يلي:

1{
2    "require": {
3        "egulias/email-validator": "^3.2"
4    }
5}

بداخل require ستجد قائمة باعتماديات مشروعك، عندما استخدمنا الأمر composer require تمت إضافة الحزمة إلى هذه القائمة، نلاحظ أيضا وجود رقم النسخة 3.2 وهي نسخة الـ Package المستخدمة.

وجود قائمة الاعتماديات الخاصة بمشروعك يعني أنك لن تضطر لتنفيذ الأمر composer require لجميع الاعتماديات في كل مرة تثبت فيها المشروع. بما أن الاعتماديات معرفة في الملف، يمكن استخدام الأمر composer install وسيقوم بتثبيت جميع الاعتماديات المطلوبة في الملف.

قبل الدخول في التفاصيل، سنستخدم الـ Package التي قمنا بتثبيتها.

استخدام الـ Packages التي تم تثبيتها عن طريق Composer

يقوم Composer بتثبيت ملفات الـ Packages داخل مجلد vendor، لذلك فإن جميع الـ Packages الموجودة في قائمة require داخل ملف composer.json سيتم تحميلها ووضعها في مجلد vendor.

كما تعلم، حتى نتمكن من استخدام أي Function أو Class من ملف PHP داخل ملف PHP آخر، فإننا نقوم بكتابة الأمر require أو include، لكن بدلا من عمل require لجميع الـ Packages التي تم تحميلها، يقوم Composer تلقائيا بإنشاء ملف باسم autoload.php يساعدك على عمل require لجميع الـ Packages دفعة واحدة

1<?php
2
3
4require __DIR__ . '/vendor/autoload.php';

لنقم بتجربة استخدام الـ Package التي قمنا بتثبيتها:

 1<?php
 2
 3require __DIR__ . '/vendor/autoload.php';
 4
 5use Egulias\EmailValidator\EmailValidator;
 6use Egulias\EmailValidator\Validation\RFCValidation;
 7
 8$validator = new EmailValidator();
 9$isEmailValid = $validator->isValid("[email protected]", new RFCValidation());
10
11echo $isEmailValid ? 'Email is valid' : 'Email is invalid';
12echo "\n";

وهذه هي النتيجة:

نتيجة تنفيذ الـ Script

لاحظ أننا عند عمل require للملف autoload.php لا نضطر لإضافة جملة require جديدة لكل Package جديدة يتم تثبيتها، أو حذف الجملة عند حذف أي Package، بل يقوم Composer بهذه العملية تلقائيا داخل ملف autoload.php.

هذه العملية تسمى Autoloading، للقراءة عنها بشكل مفصل أكثر، يمكنك مراجعة هذا الرابط Composer Autoloading

شجرة الاعتماديات - Dependency Tree

لنقم الآن بإلقاء نظرة على محتويات vendor:

محتويات مجلد Vendor

كما ترى، يوجد ملف autoload.php الذي تحدثنا عنه سابقا، كما توجد ملفات حزمة EmailValidator التي قمنا بتثبيتها في المثال السابق ضمن المسار egulias/email-validator ومجلد composer يحتوي على عدد من الملفات المتعلقة بالـ Autoloading وغيرها. لكن من أين جاءت باقي الملفات؟

سنستعرض الآن محتويات الملف vendor/egulias/email-validator/composer.json، ستجد العديد من البيانات، لكن ما يهمنا الآن هو require:

1{
2  "require": {
3    "php": ">=7.2",
4    "doctrine/lexer": "^1.2",
5    "symfony/polyfill-intl-idn": "^1.15"
6  }
7}

شرحنا سابقا عن ملف composer.json في مجلد المشروع وكيف يحتوي على قائمة الاعتماديات الخاصة بمشروعنا، وكما تلاحظ، فستجد بداخل كل Package ملف composer.json يحتوي على مجموعة من الاعتماديات الخاصة بتلك الـ Package، وهذا يذكرنا بالمثال الافتراضي المطروح في مقدمة هذا المقال.

لاحظ في قائمة require السابقة وجود php، هذا أن هذه النسخة من الحزمة لا تعمل إلا على نسخة php 7.2 أو أعلى.

هذا هو سبب وجود المجلدات التي رأيناها داخل vendor مع أننا لم نقم باختيارها كاعتماديات.

شجرة الاعتماديات / Dependency Tree الخاصة بمثال مشروع التحقق من صحة البريد الالكتروني

التعامل مع النُسخ (Versioning)

يقوم Composer بالتعامل مع نُسخ الـ Packages المطلوبة، فتجد دائما في قائمة require اسم الـ Package والنسخة المطلوب تثبيتها.

الـ Packages الخاصة بـ Composer تعتمد الـ Semantic Versioning لتحديد أرقام النُسخ، فًهمُ الـ Semantic Versioning سيساعدك على فهم إدارة الـ Versions في Composer بشكل أفضل، ويمكنك مراجعة هذا الرابط الذي يشرح هذا الأسلوب في تحديد أرقام النسخ: (الإدارة الدلالية لنُسخ البرمجيات 2.0.0 | Semantic Versioning).

محددات النُسخ في ملف composer.json

يقوم Composer بتثبيت النسخة الأنسب لمشروعك، يثبت Composer أحدث نسخة متوافقة مع مشروعك في حال عدم تحديد النسخة في ملف composer.json، أما في حالة تحديد نسخة في الملف، يقوم Composer بمحاولة تثبيتها بدلا من أحدث نسخة متوافقة:

1{
2    "require": {
3        "egulias/email-validator": "3.1.1"
4    }
5}

في المثال السابق، سيقوم Composer بتثبيت النسخة 3.1.1 ، لأنها النسخة المطلوبة، لكن بما أننا نعلم أن الرقم الثالث (Patch) متعلق باصلاح المشاكل (بحسب الـ Semver)، وهو تحديث متوافق مع النسخ السابقة (Backward Compatible)، فربما تريد من Composer أن يقوم بتثبيت الحزمة مع آخر Patch متوفر:

1{
2    "require": {
3        "egulias/email-validator": "3.1.*"
4    }
5}

في هذا المثال، سيقوم Composer بتثبيت أي نسخة تبدأ بـ 3.1، ويحاول تثبيت أحدث Patch متوفر.

عندما قمنا باستخدام الأمر composer require لتثبيت الـ Package، قام Composer تلقائيا باستخدام النسخة ^3.2 الرمز ^ قبل رقم النسخة يعني عدم السماح لـ Composer بتثبيت أي نسخة غير متوافقة مع النسخة 3.2.* هذا يعني تثبيت رقم الـ Major وتحديث ما سواه، مثال:

النسخة هل يسمح بتثبيتها؟
3.2.* نعم
3.9.9 نعم
3.1.9 لا، لأن اقل رقم Minor مسموح هو 2
4.0.0 لا، لأن الرمز ^ يمنع تحديث رقم الـ Major

لمعرفة جميع الصيغ الخاصة بمُحددات النسخ، راجع التوثيق الخاص بـ Composer

لاحظ أننا في composer.json في العادة لا نحدد النسخة بالضبط، بل نقوم بإعطاء Composer تعليمات عامة حول النسخ المسموحة في المشروع، إعطاء Composer الحرية في اختيار النسخة المناسبة والمتوافقة مع متطلبات مشروعك له عدة فوائد، كالحصول على تحديثات لاعتمادياتك ضمن ضوابط وشروط تضمن عدم تعطل المشروع عن طريق عدم الموافقة على التحديث إلى نسخ تحتوي على Breaking Changes، مما يضمن حصولك على المزايا الجديدة والإصلاحات للاخطاء باستمرار لاعتماديات مشروعك.

ستجد في العادة أن الرمز ^ كثير الاستخدام، لأنه يمنع من تحديث الـ Major Version، وهذا مهم لاستقرار المشروع، راجع هذا الرابط لتفاصيل أكثر Why are unbound version constraints a bad idea? - Composer

كيف يختار Composer النسخة المناسبة

يحاول Composer تثبيت أحدث نسخة (ضمن المحددات الموجودة في ملف composer.json)، لكن شجرة الاعتماديات (Dependency Tree) قد تكون معقدة ومترابطة، بحيث تجد أن الكثير من الاعتماديات في مشروعك تعتمد على Package محددة.

ومما يزيد الأمور تعقيدا، أن كل واحد من هذه الاعتماديات له محددات نسخ (Versions Constrants) مختلفة، لذلك يحاول Composer تحديد النسخة الأحدث المتوافقة مع جميع هذه الاعتماديات، ويستبعد النسخ التي تتوافق مع بعض الاعتماديات وتتعارض مع بعضها.

في المثال الموضح بالصورة التالية، نعتمد في مشروعنا على package-a و package-b وكلاهما يعتمدان على package-c

صورة شجرة الاعتماديات الخاصة بشرح اختيار النسخة المناسبة من الحزمة

لنفترض أن هذا هو ملف composer.json الخاص بـ package-a:

1{
2    "require": {
3        "package-c": "^5.0||^6.0"
4    }
5}

وهذا ملف composer.json الخاص بـ package-b:

1{
2    "require": {
3        "package-c": "^6.0||^7.0"
4    }
5}

علامة || في محددات النسخ تعني (Or) مما يمكنك من دعم أكثر من Major Version لمشروعك بسهولة

في هذه الحالة سيقوم Composer بتثبيت أحدث نسخة متوفرة ضمن الـ Major Version رقم “6” من الـ package-c، على الرغم من وجود النسخة 7.0 ضمن متطلبات package-b إلا أن النسخ التي تتوافق مع جميع الاعتماديات هي النسخ الأعلى من 6.0 وأقل من 7.0.

النسخ المُثبتة باستخدام composer.lock

الـ Semantic Versioning مفيد جدا، بحيث أن تعديل آخر مقطعين من رقم النسخة (Minor, Patch) يفترض أن يضمن التوافقية مع النسخ السابقة (Backward Compatability) وعدم وجود (Breaking Changes).

لكن في الواقع، تحديد أرقام هذه النسخ يتم من مطور الـ Package بشكل يدوي، المطور أو المبرمج هو من يحدد ما إذا كان التعديل يحتوي على Breaking Changes أم لا، ويعدل أرقام النسخ بناء على ذلك.

بما أن العملية تتم بشكل يدوي، فحدوث الخطأ وارد جدا، نظريا يفترض أن التحديث من النسخة 5.3.4 إلى النسخة 5.7.2 لن يسبب أي مشاكل، فالتحديث هو لرقم الـ Minor، أي أنه يضيف مزايا جديدة دون الإخلال بالمزايا القديمة، بالإضافة لرقم الـ Patch، أي إصلاح المشاكل دون تأثير على المزايا القديمة، لكن ماذا لو أن مطور ال package قام دون انتباه بتعديل غير متوافق مع النسخ السابقة (Backward Incompatible Update)؟

تخيل مثلا أنك تقوم بالعمل على المشروع من جهازك الخاص، النسخة المثبتة من الحزمة التي تسبب المشكلة على جهازك هي 5.3.4، ثم قمت برفع تعديلاتك إلى سيرفر البيئة الانتاجية (Production Server)، وعلى ذلك السيرفر تم تثبيت النسخة 5.7.2 والتي يفترض نظريا أن لا تسبب مشكلة، لكنها في الواقع تحتوي على مشكلة تعطل المشروع الخاص بك.

يقوم Composer بحل هذه المشكلة عن طريق ملف composr.lock، عادة تقوم في ملف composer.json بتحديد عدة نسخ محتملة، فمثلا تقوم بتحديد ^5.0 والذي يحتمل النسخة 5.0.0 وأي نسخة أقل من 6.0.0، بينما في ملف composer.lock سيتم (بشكل آلي دون تدخل منك) بتحديد رقم النسخة المثبتة بشكل دقيق دون احتمالات.

في المثال السابق الذي افترضنا فيه أن Composer قام بتثبيت النسخة 5.3.4 على جهازك، سيقوم Composer أيضا بإضافة النسخة 5.3.4 إلى ملف composer.lock، وبعد الرفع على السيرفر وتنفيذ الأمر composer install سيقوم Composer بتثبيت النسخة 5.3.4 لأنها النسخة الموجودة في ملف composer.lock ولن يقوم بتثبيت النسخة 5.7.2، وهكذا ستضمن أن النسخة التي كنت تعمل عليها هي نفسها النسخة التي سيتم تثبيتها على السيرفر.

من المهم إضافة composer.lock إلى المستودع الخاص بمشروعك في Git ورفعه على السيرفر مع ملفات المشروع، لا تقم أبدا بإضافة composer.lock إلى ملف .gitignore

أوامر تثبيت الحزم وعلاقتها بالملفين composer.json و composer.lock

سنستعرض الآن ثلاثة أوامر متعلقة بتثبيت الحزم:

  • composer require {package_name}
  • composer install
  • composer update

composer require {package_name}

هذا الأمر يستخدم لتثبيت حزمة جديدة، قمنا باستخدام هذا الأمر سابقا لتثبيت حزمة Email Validator، هذا الأمر يقوم بتعديل ملفي composer.json و composer.lock أو يقوم بإنشائهما في حال عدم وجودهما بين ملفات المشروع.

composer install

هذا الأمر سيثبت الاعتماديات من ملف composer.json لذلك يجب استخدام هذا الأمر على السيرفرات لضمان ثبات النسخ وعدم تحديثها، لا يقوم هذا الأمر بتعديل أي ملفات في العادة، لكن إن لم يكن ملف composer.lock موجودا فسيقوم بإنشائه.

composer update

هذا الأمر سيقوم بتحديث الحزم (مع الأخذ بعين الاعتبار محددات النسخ في composer.json)، يقوم هذا الأمر بتحديث ملف composer.lock لإضافة أرقام النسخ الجديدة.

نصائح عند التعامل مع Composer

في ختام هذا المقال، أضع لكم نصائح عامة متعلقة بـ Composer أثناء التعامل مع Git أو التعامل مع السيرفرات، هذه النصائح هي قواعد عامة وقد توجد حالات قليلة استثنائية يمكن عدم تطبيق هذه الإرشادات فيها.

التعامل مع Git

يجب اضافة ملفي composer.json و composer.lock إلى مستودع Git، إضافتهما ستمكن جميع المطورين (والسيرفرات) من الحصول على نفس نسخ الحزم بالضبط في حال التثبيت باستخدام composer install.

على العكس من ملفات Composer، يجب إضافة مجلد vendor إلى .gitignore، مجلد vendor عادة ما يكون حجمه كبيرا جدا، ولا فائدة من وجوده في المستودع، لأن الحزم يمكن تثبيتها بسهولة بمجرد توفرها في ملف composer.json، كما أن وجود مجلد vendor في المستودع قد يشجع بعض المبرمجين قليلي الخبرة على التعديل على ملفات الحزم بشكل مباشر داخل مجلد vendor مما يجعل من الصعب إضافة حزم جديدة أو تحديث الحزم الحالية.

ستجد تفاصيل أكثر عن إضافة مجلد vendor إلى مستودع Git في هذا الرابط Should I commit the dependencies in my vendor directory? - Composer

التعامل مع السيرفرات

دائما قم باستخدام composer install على السيرفر بدلا من composer update، لأن الأمر install يقوم بتحميل أرقام النسخ المثبتة في composer.lock مما يضمن الاستقرارية (Stability) بشكل أكبر، كما أنه لا يقوم بالتعديل على الملفات، مما يعني أنه لن يسبب Conflicts عند عمل git pull لمستودع المشروع في ملفات Composer.

اقرأ أيضا