تشریح PSR-4 برای پیاده سازی autoload استاندرد با namespace

سلام.
در این مقاله میخوام قوانین ساخت یک autoloader استاندارد رو بر اساس PSR-4 توضیح بدم.
سعی شده، مطالب بصورت ساده و روان بهمراه نکات و مثال هایی روی دیگر پروژه های استاندارد گفته بشه.در مورد namespace ها هم نکاتی آورده شده تا درک مناسب و بهتری در این موضوع پیدا کنید.

1. قوانین این لیست برای class ها ، interface ها ، trait ها و دیگر ساختارهای مشابه قابل پیاده سازی است و هر جا کلمه ی class یا کلاس اومد اشاره به همین ها دارد.

2. یک namespace کامل به شکل زیر ذکر میشود:

\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>

++ بک اسلش (\) ابتدایی، به Global Space اشاره میکند. اگر خواستید کلاسی رو که در یک space دیگر قرار دارد داخل یک namespace بیاورید میبایست ابتدای آن یک بک اسلش(\) بگذاریم:

<?php
namespace A\B\C;

$dt = new DateTime();
echo $dt->getTimestamp() . PHP_EOL;

در کد بالا یک Fatal Error رخ میدهد:

Fatal error: Uncaught Error: Class 'A\B\C\DateTime' not found

علتش اینه که ما در فضای A\B\C هستیم و در این فضا دنبال کلاس DateTime میگرده. در صورتی که این تابع جزو توابع خود PHP هست و در فضای global قرار دارد. پس یک \ اضافه میکنیم تا بگیم از global space دنبال کلاس DateTime بگرده:

<?php
namespace A\B\C;

$dt = new \DateTime();
echo $dt->getTimestamp() . PHP_EOL;
// output: 1591927525

در غیر اینصورت نیازی به آوردنش نیست. اگر با استفاده از use کلاس را import میکنید هم فرقی نمیکند بیاورید یا نه.
با توجه به توضیحات، این مثال رو میتونم به دو شکل بنویسم و صحیح است:

<?php
namespace A\B\C;

$a = (new \classes\base\Hello)->sayHelloWorld();

یا بوسیله ی use کلاس رو import کنم:

namespace A\B\C;

use classes\base\Hello;

$a = (new Hello)->sayHelloWorld();

تذکر: به یک نکته توجه کنید، سینتکس تعریف namespace میبایست بعنوان اولین دستور در فایل PHP نوشته شود و قبل از تگ php هم هیچ کاراکتر و whitespace ای نباید در خروجی قرار بگیره وگرنه خطای زیر تولید میشه:

[whitespace]
<?php
namespace A\B\C;

Fatal error: Namespace declaration statement has to be the very first statement or after any declare call in the script in ...
بنابراین به این مورد هم دقت داشته باشید که encoding فایلتون روی UTF-8 with BOM نباشه وگرنه باز هم این خطا نمایش داده میشه.(بخاطر وجود byte order mark در ابتدای فایل)

++ namespace ها میتونن چندین level تو در تو داشته باشن ولی اگر هیچ sub-level ای در نظر نگرفتید، حداقل باید یک top-level داشته باشند تا یک namespace در فضای global قرار نگیرد. برای مثال classes\Hello.php در اینجا classes یک top-level محسوب میشود.

++  پس با توجه به جمله ی قبلی، SubNamespace ها میتونه یک یا چنتا بیاد. classes\base\App.php
++  ساختار namespace با اسم کلاس خاتمه پیدا میکنه. App.php
++ در این ساختار Underscore ها(_) هیچ معنی خاصی ندارن.
++ حروف الفبا میتونه ترکیبی از حروف کوچک و بزرگ باشد.
++ نام کلاس باید به حروف کوچک و بزرگ حساس باشد. یعنی در ساختار تابع autoload نام کلاس رو همونطوری که هست بگیرید و در تابع strtolower نگذارید.

 

3. بعد از رعایت قوانین گفته شده در ادامه، نکات مهمی از PSR-4 رو میخوام توضیح بدم تا انتها بهمراه مثال هاش بخونید که متوجه بشید، موضوع چیه:

++ برای جلوگیری از دایرکتوری های تو در تو (اصطلاحاً nested directories) ترجیحاً میتونید بخشی از namespace رو بعنوان یک namespace prefix در نظر بگیرید که این prefix به مسیری اشاره میکند که فایل های پروژه داخلش قرار گرفتن(base directory)
پروژه میتونه prefix های مختلفی بهمراه مسیرهای متفاوتی باشه.
برای اینکه این مطلب بهتر جابیوفته، نکته ی بعدی رو هم بیارم بعد مثال میزنم.

++ اسامی هر sub-namespace که بعد از prefix اومده مطابق با یک sub directory درون base directory خواهد بود که این sub directory باید همنام(از نظر کوچک و بزرگی هم مطابقت داشته باشد) با اسامی sub-namespace ها باشد. ضمناً جداکننده namespace (یعنی \ بک اسلش ها) در مسیردهی آدرس فایل روی سیستم با ثابت DIRECTORY_SEPARATOR باید replace شوند. (مسیر دایرکتوری ها در لینوکس با / از هم جدا میشوند.)

++ نام کلاسی که در انتهای namespace آمده است باید با یک فایل با پسوند .php هم نام باشد.(از نظر کوچک و بزرگی هم مطابقت داشته باشد)

 

چنتا مثال از خود داک و جاهای دیگه میارم.
داک PHPFig:

FULLY QUALIFIED CLASS NAME      	   NAMESPACE PREFIX      	   BASE DIRECTORY      	   RESULTING FILE PATH
\Acme\Log\Writer\File_Writer	                Acme\Log\Writer	           ./acme-log-writer/lib/	   ./acme-log-writer/lib/File_Writer.php
\Aura\Web\Response\Status	                    Aura\Web        	           /path/to/aura-web/src/     /path/to/aura-web/src/Response/Status.php
\Symfony\Core\Request	                        Symfony\Core	       ./vendor/Symfony/Core/      ./vendor/Symfony/Core/Request.php
\Zend\Acl	                                            Zend	             /usr/includes/Zend/	   /usr/includes/Zend/Acl.php

 

فریمورک Yii:
برای مثال در این فریمورک namespace ای با عنوان yii\base\Application یا yii\web\Application یا yii\BaseYii و ... وجود دارد. این فریمورک طبق این مواردی که گفتیم، path رو در یک آرایه به اسم aliases نگه میداره. در این namespace هایی که گذاشتم top-level یعنی yii رو در آرایه ای با نام aliases مسیردهی میکنه به این شکل که ایندکسش رو @yii قرار داده و مقدارش آدرسی مطابق با مسیر دایرکتوری yii2 در نظر گرفته میشه path/to/project_directory/vendor/yiisoft/yii2 ست میشه.
بنابراین اگر بنویسیم:
* use yii\base\Application منظورمان فایل Application.php مثلا در همچین مسیری است:

/var/www/html/yiiblog/vendor/yiisoft/yii2/base/Application.php

* use yii\web\Application:

/var/www/html/yiiblog/vendor/yiisoft/yii2/web/Application.php

* use yii\BaseYii:

/var/www/html/yiiblog/vendor/yiisoft/yii2/BaseYii.php

البته در فریمورک Yii، مسیر کلاس های داخل فریمورک در آرایه ی classMap تعریف شده. بخشی ازین آرایه رو ببینید:

return [
  'yii\base\Action' => YII2_PATH . '/base/Action.php',
  'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',
  'yii\base\ActionFilter' => YII2_PATH . '/base/ActionFilter.php',
  'yii\base\Application' => YII2_PATH . '/base/Application.php',
  // ....
  // ....
  // etc....
];

ثابت YII2_PATH و yii alias یعنی @yii مسیر مشترکی دارند و به مسیر دایرکتوری yii2 اشاره میکنند.
اگر namespace ای در classMap نبود Autoload میره از prefix namespace هایی که مسیرشون در آرایه ی aliases ست شده، کمک میگیره برای پیدا کردن فایل ها و ...

 

Composer:
پکیج هایی که در vendor قرار دارند رو اگر باز کنید، هر کدوم فایل composer.json دارند که داخل این فایل و در قسمت autoload psr-4 مسیر prefix namespace ها مشخص شده. یعنی مشخص شده فایل های مرتبط با prefix در چه مسیری قرار گرفته.
برای مثال اگر پکیج doctrine رو باز کنید: path/to/project_name/vendor/doctrine/instantiator
در فایل composer.json آمده:

"autoload": {
    "psr-4": {
        "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
    }
},

* در \\ بک اسلش اول برای escape کردن بک اسلش دوم (در داخل رشته) اومده.
با توجه به مسیر هر پکیج خود composer هم یک map داره که مسیردهی پکیج هارو در فایل path/to/project_name/vendor/composer/autoload_psr4.php مپ کرده. بخشی ازین فایل:

<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'yii\\composer\\' => array($vendorDir . '/yiisoft/yii2-composer'),
    'yii\\bootstrap\\' => array($vendorDir . '/yiisoft/yii2-bootstrap/src'),
    'yii\\' => array($vendorDir . '/yiisoft/yii2'),
    'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
    // ....
    // ....
    // etc....
);

 

خب بند آخر PHPFig رو هم بیارم و تمام.
4. autoloader شما نباید هیچ exception یا خطایی تولید کنه و توصیه میشه که هیچ مقداری هم return نشه.
(البته من دیدم توو پروژه ها exception تولید میکنن)
این مثال رو برای پیاده سازی یک autoload ببینید.


مطالب مرتبط

برای ارسال نظر، باید عضو سایت شوید.