سلام.
در این مقاله میخوام قوانین ساخت یک autoloader استاندارد رو بر اساس PSR-۴ توضیح بدم.
در این مقاله سعی شده، مطالب بصورت ساده و روان بهمراه نکات و مثال هایی روی دیگر پروژه های استاندارد گفته بشه.در مورد namespaceها هم نکاتی آورده شده تا درک مناسب و بهتری در این موضوع پیدا کنید.
۱. قوانین این لیست برای classها ، interfaceها ، traitها و دیگر ساختارهای مشابه قابل پیاده سازی است و هر جا کلمه ی class یا کلاس اومد اشاره به همینها دارد.
۲. یک 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-۸ 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 نگذارید.
۳. بعد از رعایت قوانین گفته شده در ادامه، نکات مهمی از PSR-۴ رو میخوام توضیح بدم تا انتها بهمراه مثال هاش بخونید که متوجه بشید، موضوع چیه:
++ برای جلوگیری از دایرکتوریهای تو در تو (اصطلاحاً 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 هم نام باشد.(از نظر کوچک و بزرگی هم مطابقت داشته باشد)
چنتا مثال از خود داک و جاهای دیگه میارم.
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 قرار داده و مقدارش آدرسی مطابق با مسیر دایرکتوری yii۲ در نظر گرفته میشه path/to/project_directory/vendor/yiisoft/yii۲ ست میشه.
بنابراین اگر بنویسیم:
* 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....
];
ثابت YII۲_PATH و yii alias یعنی @yii مسیر مشترکی دارند و به مسیر دایرکتوری yii۲ اشاره میکنند.
اگر 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_psr۴.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 رو هم بیارم و تمام.
۴. autoloader شما نباید هیچ exception یا خطایی تولید کنه و توصیه میشه که هیچ مقداری هم return نشه.
(البته من دیدم توو پروژهها exception تولید میکنن)
این مثال رو برای پیاده سازی یک autoloader ببینید.
نویسنده: محسن موحد