PHP یک زبان اسکریپت نویسی محبوب است که توسط حدود 79٪ از وب سایت های دنیا استفاده می شود. این زبان هشت برابر محبوب تر از نزدیکترین رقیب خود یعنی ASP.NET می باشد. چگونگی نوشتن کد های php سودآور، یادگیری آن آسان و سرعت اجرای بالایی دارد و با اکثر پلتفرم ها، پایگاه داده ها و مرورگرها سازگار است. محبوبیت PHP باعث شده تا جامعه بزرگی از توسعه دهندگان برای آن ایجاد شود. تعداد بالای متخصصین، منجر به رقابت بالا و درخواست حقوق پایین تر شده است که برای کاهش هزینه های توسعه مفید خواهد بود.
محبوبیت PHP، نتیجه ای منطقی از مزایای فراوان آن است. این مزایا، PHP را به یک ابزار توسعه قدرتمند و موثر تبدیل می کنند اما با وجود سادگی استفاده از آن، به یک زبان پیچیده تبدیل شده که دارای جزئیات بسیار زیاد است. همین جزئیات می توانند برنامهنویسان را گیج کنند و منجر به صرف ساعتها خطایابی (debug) گردد. این مقاله به ده خطای رایجتر که برنامهنویسان در نوشتن کد های php باید از آنها دوری کنند، می پردازد.
اشتباه رایج اول: رها کردن ارجاعات آرایه پس از اجرای حلقه foreach
قطعه کد زیر را در نظر بگیرید:
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
echo $value."*2 : ";
$value = $value * 2;
echo $value."<br>";
}
در این کد از علامت & استفاده شده است که باعث میشود متغیر value$ به عنوان یک مرجع به هر عنصر آرایه arr$ اشاره کند. بنابراین، هر تغییری که در value$ ایجاد کنید، در آرایه arr$ نیز منعکس میشود. عملیات بعدی روی value$، ممکن است ناخواسته آخرین عنصر آرایه arr$ را تغییر دهد. برای نمونه اگر کد ذکر شده به شکل زیر ادامه داشته باشد:
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
echo $value."*2 : ";
$value = $value * 2;
echo $value."<br>";
}
foreach ($arr as $value) {
echo $value.PHP_EOL;
}
وجود & در حلقه اول باعث می شود که آخرین عنصر آرایه arr$ تغییر کند و مقدار خود را به value$ بدهد. به همین دلیل، در آخرین تکرار حلقه، دو بار عدد 6 چاپ میشود. نتیجه اجرا در تصویر (1) قابل مشاهده است.
تصویر(1)
با اجرای تابع foreach دوم مشخص می شود که آخرین مقدار در آرایه arr$ برابر با 6 است اما چرا چنین مشکلی رخ می دهد؟
پس از عبور از اولین حلقه foreach، آرایه arr$ بدون تغییر باقی می ماند اما، همانطور که در بالا توضیح داده شد، value$ به عنوان یک مرجع اشاره کننده به آخرین عنصر در آرایه arr$ تنظیم شده است.
از آنجایی که، foreach هر عنصر آرایه arr$ را در هر مرحله از حلقه، درون value$ کپی می کند. با در نظر گرفتن آدرس دهی عناصر آرایه arr$ به صورت {8=[3]arr و 6=[2]arr و 4=[1]arr و 2=[0]arr}، بررسی خواهد شد که در طول هر مرحله از حلقه foreach دوم چه اتفاقی رخ می دهد:
- کپی کردن [0]arr$ درون متغیر value$ که خود این متغیر در حلقه foreach اول یک اشاره گر می باشد. زمانی که در متغیر value$ محتوایی کپی می شود، خود value$ در حال اشاره به [3]arr$ خواهد بود. این موضوع نیز در آخرین اجرای حلقه اول به value$ منتسب شده است. در نتیجه پس از اولین اجرای حلقه foreach دوم، آرایه arr$ به صورت [2,4,6,2] خواهد بود.
- در دومین اجرای حلقه foreach دوم، [1]arr$ به value$ کپی می گردد. از جایی که هنوز value$ به آخرین عنصر آرایه arr$ یعنی [3]arr$ اشاره می کند، مقدار آخرین عضو آرایه arr$ تغییر کرده و نتیجه آن به صورت [2,4,6,4] خواهد بود.
- در سومین اجرای حلقه foreach دوم، [2]arr$ به value$ کپی می گردد. از جایی که هنوز value$ به آخرین عنصر آرایه arr$ یعنی [3]arr$ اشاره می کند، مقدار آخرین عضو آرایه arr$ تغییر کرده و نتیجه آن به صورت [2,4,6,6] خواهد بود.
- اما در اجرای چهارم مقدار [3]arr$ به value$ کپی می گردد که این معادل کپی خود [3]arr$ به [3]arr$ بوده و به همین دلیل نتیجه اجرای آخر حلقه دوم با اجرای یکی به آخر آن فرقی نداشته و عناصر آرایه arr$ معادل [2,4,6,6] خواهند بود.
جهت جلوگیری از این مشکل، شما باید پس از پایان حلقه foreach اول، متغیر value$ را با تابع ()unset آزاد نمایید تا ارتباط آن با آخرین عنصر آرایه قطع گردد.
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
echo $value."*2 : ";
$value = $value * 2;
echo $value."<br>";
}
unset($value);
foreach ($arr as $value) {
echo $value.PHP_EOL;
اشتباه رایج دوم: برداشت اشتباه از عملکرد تابع ()isset
در نوشتن کد های php باید در نظر داشت که تابع ()isset هم برای عدم وجود عناصر و هم زمانی که مقدار null به آن ارسال گردد، نتیجه false را برمیگرداند. این رفتار تابع ()isset بیشتر از آن چیزی که تصور می شود، باعث ایجاد مشکل خواهد شد. قطعه کد زیر را در نظر بگیرید:
$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
// اجرای دستورات بعدی
}
احتمالاً نویسنده این کد در نظر داشته است تا بررسی کند آیا keyShouldBeSet در data$ تنظیم شده است یا خیر. با این حال، همانطور که ذکر شد، اگر مقدار data$ روی null تنظیم شده باشد، تابع isset پس از بررسی (data['keyShouldBeSet']$) مقدار false را برمی گرداند. بنابراین منطق فوق ناقص است. برای درک بیشتر به مثال دوم توجه نمایید:
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 را نشان دهد.
اما اینگونه نیست، همانطور که توضیح داده شد، اگر postData$ روی null باشد، isset($postData) نیز false را برمی گرداند. بنابراین ممکن است برای isset($postData) نیز false برگشت داده شود حتی اگر POST['active']_$ برابر با true باشد. پس باز هم منطق بالا ناقص خواهد بود. به هر حال، اگر هدف کد بالا این بود تا دوباره بررسی شود که آیا POST['active']_$ درست است یا خیر، تکیه بر ()isset تصمیم کدنویسی اشتباهی خواهد بود.
اشتباه رایج سوم: سردرگمی در مورد بازگرداندن مقدار از طریق reference یا value
جهت بررسی این اشتباه رایج در نوشتن کد های php، به قطعه کد زیر توجه نمایید:
class Config
{
private $values = [];
public function getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
در صورت اجرای قطعه کد بالا با خطایی شبیه به آنچه در تصویر(2) رخ داده، روبرو خواهید شد:
تصویر(2)
مسئله این است که کد بالا، آرایههای برگشتی از طریق ارجاع را با آرایههای برگشتی بر اساس مقدار اشتباه میگیرد. مگر اینکه صریحاً به PHP بگویید که یک آرایه را با مرجع برگرداند (یعنی با استفاده از کاراکتر &). PHP به طور پیشفرض آرایه را بر اساس مقدار برگشت می دهد. این بدان معنی است که یک کپی از آرایه برگردانده می شود و بنابراین تابع فراخوانده شده و درخواست کننده، به همان نمونه آرایه دسترسی نخواهند داشت.
در نتیجه، فراخوانی فوق به ()getValues یک کپی از آرایه values$ را اختصاص می دهد. با در نظر گرفتن این موضوع، دو خط کلیدی مثال بالا دوباره بررسی میشود:
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
تابع ()getValues یک مقدار با عنوان test را به کپی از آرایه values$ اضافه می کند اما به خود آرایه مقداری اضافه نمی شود. سپس تابع ()getValues یک کپی دیگر از values$ را برمیگرداند که شامل test نیست و به همین دلیل خطا با متن undefined مشاهده می شود. یک راه حل این است که اولین نسخه برگشتی از آرایه values$ توسط ()getValues، ذخیره گردد و سپس روی آن کار شود:
$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];
کد فوق به خوبی کار می کند اما با توجه به آنچه که می خواهید انجام دهید، این رویکرد ممکن است کافی یا ناکافی باشد. به طور خاص، کد بالا آرایه اصلی values$ را تغییر نخواهد داد. بنابراین اگر می خواهید تغییرات شما (مانند افزودن یک عنصر "test") روی آرایه اصلی تاثیر بگذارد، باید تابع ()getValues را تغییر دهید تا یک ارجاع به خود آرایه values$ برگردانده شود. این کار با اضافه کردن یک & قبل از نام تابع انجام می گردد.
class Config
{
private $values = [];
public function &getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
همانطور که انتظار می رود، خروجی این آزمایش، صحیح خواهد بود:
تصویر(3)
اما برای اینکه کمی پیچیده تر گردد، به جای کد بالا، قطعه کد زیر در نظر گرفته شود:
class Config
{
private $values;
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 می شود، اشتباه کرده اید. در واقع، این کد به خوبی کار خواهد کرد. برخلاف آرایه ها، PHP همیشه اشیاء را با مرجع ارسال می کند.
همانطور که این مثالها نشان میدهند، در نوشتن کد های php کاملاً مشخص نیست که آیا با یک نسخه کپی از داده ها سروکار دارید یا یک مرجع را خطاب قرار می دهید. بنابراین درک این رفتارهای پیشفرض، ضروری است. همچنین به دقت راهنمای API را برای تابعی که فراخوانی میکنید بررسی نمایید تا متوجه شوید آیا آن مقدار، کپی از یک آرایه، ارجاع به آرایه یا ارجاع به یک شی را برمیگرداند.
توجه به این نکته مهم است که باید از عمل بازگرداندن یک مرجع به یک آرایه یا ArrayObject، اجتناب کرد. زیرا این امکان را برای درخواست کننده فراهم می کند تا داده های خصوصی را تغییر دهد. در این صورت، ایزوله سازی کد بی معنی می شود و میتوان آن را دور زد. در مقابل، بهتر است از «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');
این رویکرد به درخواست کننده امکان میدهد تا هر مقداری را در آرایه تنظیم یا از آن دریافت کند، بدون اینکه دسترسی عمومی به آرایه values$ خصوصی داشته باشد.
تصویر(4)
اشتباه رایج چهارم: اجرای query ها در حلقه ها
اگر در نوشتن کد های 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 کوئری جداگانه برای منبع ایجاد می کند. اگر چنین اسکریپتی در چندین رشته فراخوانی شود، توانایی از دسترس خارج کردن سیستم را خواهد داشت.
بنابراین بسیار مهم است که تشخیص دهید چه زمانی کوئریها توسط کد شما ساخته میشوند. در صورت امکان، باید مقادیر را جمعآوری نمایید و سپس یک کوئری را برای واکشی تمامی نتایج اجرا کنید.
یک نمونه رایج از اجرای ناکارآمد، زمانی است که فرمی به همراه لیستی از مقادیر مانند شناسه ها، ارسال می شود. سپس، جهت بازیابی اطلاعات کامل هر یک از شناسه ها، حلقه ای اجرای می شود که طی هر دور اجرای آن، یک کوئری در دیتابیس شکل می گیرد. این عمل اغلب شبیه به کد زیر خواهد بود:
$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;
}
}
بنابراین بسیار مهم است که تشخیص دهید چه زمانی کوئری های دیتابیس به طور مستقیم یا غیرمستقیم توسط کد شما ایجاد می شوند. در صورت امکان، مقادیر را جمع آوری نمایید و سپس یک کوئری را برای واکشی همه نتایج اجرا کنید.
اشتباه رایج پنجم: استفاده غیر واقعی و غیر بهینه از memory
در حالی که واکشی بسیاری از رکوردها به صورت همزمان، کارآمدتر از اجرای کوئری های جداگانه برای هر سطر است، چنین رویکردی می تواند منجر به وضعیت «out of memory» در libmysqlclient هنگام استفاده از PHP mysql شود. برای نمونه در نوشتن کد های php، باید محیط آزمایشی با منابع محدود (512 مگابایت رم)، MySQL و php-cli مورد بررسی قرار گیرد. در کد زیر اقدام به ایجاد یک جدول توسط php می شود:
$connection = new mysqli('localhost', 'username', 'password', 'database');
$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);
// ایجاد دو میلیون سطر داده
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);
}
توجه: در محیط تست، اجرای این کد آن هم فقط برای 20 ثانیه، اطلاعاتی با حجم معادل 1.3 گیگابایت در جدول test وارد نمود. از اجرا و تست این کد در هاست خود اجتناب نمایید. جهت بررسی منابع می توانید کد زیر را اجرا کنید:
// connect to 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 /example/index.php on line 11
در این بخش از نوشتن کد های php، مشکل نحوه کار ماژول PHP mysql می باشد. PHP mysql در واقع فقط یک پروکسی برای libmysqlclient است که کار اصلی را انجام می دهد. در PHP mysql وقتی بخشی از داده ها انتخاب می شوند، مستقیماً به حافظه منتقل می گردند. از آنجایی که این حافظه توسط PHP مدیریت نمی شود، در واقع زمانی که برنامه نویس محدودیت را در کوئری خود افزایش هد، ()memory_get_peak_usage هیچ افزایشی در استفاده از منابع را نشان نمی دهد. همین موضوع نیز منجر به مشکلاتی مانند آنچه در بالا نشان داده شد، میشود.
با استفاده از ماژول mysqlnd می توانید از headfake هایی مانند مثال بالا اجتناب گردد. بنابراین، اگر تست فوق با استفاده از mysqlnd به جای mysql اجرا شود، تصویر بسیار واقعی تری در رابطه با استفاده از حافظه دریافت می گردد:
Before: 232048
Limit 1: 324952
Limit 10000: 32572912
طبق مستندات زبان PHP، متاسفانه mysql برای ذخیره داده ها دو برابر mysqlnd از منابع استفاده می کند. بنابراین اسکریپت اصلی با استفاده از 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");
}
اشتباه رایج ششم: نادیده گرفتن Unicode/UTF-8
در نوشتن کد های php به Unicode/UTF-8 آن چنان که باید، پرداخته نشده است. هسته PHP 6 قرار بود Unicode-aware (به این معنی که قادر خواهد بود تا تمام کاراکترهای Unicode را شناسایی و پشتیبانی کند) باشد. متاسفانه زمانی که توسعه PHP 6 در سال 2010 به حالت تعلیق درآمد، این کار نیز متوقف شد.
اما این موضوع به هیچ وجه توسعهدهنده را از ارائه صحیح UTF-8 و اجتناب از این فرض اشتباه مبنی بر اینکه همه رشتهها لزوماً "ASCII قدیمی ساده" هستند، مبرا نمیکند. حتی درخواست های ساده strlen ($_POST['name']) نیز میتوانند زمانی که فردی با نام خانوادگی مانند "Schrödinger" قصد ثبت نام در سیستم شما را دارد، باعث ایجاد مشکل شوند.
جهت جلوگیری از این نوع مشکلات می توانید مراحل زیر را طی نمایید:
- اگر در مورد Unicode و UTF-8 اطلاعات زیادی ندارید، حداقل باید اصول اولیه را یاد بگیرید.
- مطمئن شوید که همیشه از توابع mb_* به جای توابع رشته ای قدیمی استفاده می کنید.
- اطمینان حاصل نمایید که پایگاه داده و جداول شما برای استفاده از Unicode تنظیم شده اند (بسیاری از بیلدهای MySQL هنوز به طور پیش فرض از latin1 استفاده می کنند).
- به یاد داشته باشید که ()json_encode نمادهای غیر ASCII را تبدیل می کند. به عنوان مثال، "Schrödinger" تبدیل به "Schr\u00f6dinger" می شود. می توان با استفاده از تابع ()serialize این مشکل را رفع کرد.
- مطمئن شوید که فایلهای php شما دارای کد UTF-8 هستند.
اشتباه رایج هفتم: تصور این که POST_$ همواره حاوی اطلاعات پست شده است
بر خلاف نام آن، آرایه POST_$ همیشه حاوی داده های POST شده نخواهد بود و می تواند خالی از هر گونه اطلاعات باشد. برای نمونه، فرض کنید درخواست سرور با فراخوانی ()jQuery.ajax به صورت زیر انجام می گردد:
$.ajax({
url: 'http://example.com/some/path',
method: 'post',
data: JSON.stringify({a: 'a', b: 'b'}),
contentType: 'application/json'
});
در این مدل نوشتن کد های php، داده ها به صورت JSON ارسال می شوند که برای استفاده در API ها بسیار مناسب می باشد.
در مثال فوق، به راحتی می توانید، آرایه POST_$ را از سمت سرور حذف کنید:
var_dump($_POST);
نتیجه بدین صورت خواهد بود:
array(0) { }
اما چه اتفاقی برای رشته JSON {a: 'a', b: 'b'} افتاد؟
PHP فقط زمانی یک POST را بهطور خودکار تجزیه (parses) میکند که دارای محتوا از نوع application/x-www-form-urlencoded یا multipart/form-data باشد. دلایل این امر ریشه در گذشته دارد. این دو نوع محتوا، تنها مواردی بودند که در زمان ارائه POST_$ پیادهسازی و استفاده میشدند. بنابراین با هر نوع محتوای دیگر (حتی آنهایی که امروزه بسیار محبوب هستند، مانند application/json)، به طور خودکار بارگذاری POST انجام نمی شود.
از آنجایی که POST_$ متغیری فوق سراسری (superglobal) است، اگر رونویسی شود (به خصوص در ابتدای اسکریپت)، مقدار اصلاح شده در سراسر کد قابل ارجاع خواهد بود. POST_$ معمولاً توسط فریم ورک های PHP و تقریباً تمام اسکریپتهای سفارشی برای استخراج و تبدیل دادههای درخواست استفاده میشود.
بنابراین، برای مثال، هنگام پردازش یک payload POST با محتوای application/json، باید به صورت دستی محتویات درخواست را تجزیه و تحلیل نمایید (یعنی دادههای JSON را رمزگشایی کرد) و متغیر POST_$ را به صورت زیر مقدار دهی کنید
$_POST = json_decode(file_get_contents('php://input'), true);
اشتباه رایج هشتم: تصور این که PHP از نوع داده کاراکتری پشتیبانی می کند
جهت درک بیشتر این بخش برای نوشتن کد های php، بهتر است ابتدا مثال زیر بررسی شود:
for ($c = 'a'; $c <= 'z'; $c++) {
echo $c . "\n";
}
این کد، از "a" تا "z" را چاپ می کند اما سپس "aa" تا "yz" را نیز چاپ می نماید. خروجی کد در تصویر(5) قابل مشاهده است.
تصویر(5)
در PHP هیچ نوع داده char وجود ندارد. به همین دلیل با گسترش رشته z در PHP رشته aa به دست می آید. جهت چاپ حروف a تا z می توانید از کد زیر استفاده نمایید:
$letters = range('a', 'z');
for ($i = 0; $i < count($letters); $i++) {
echo $letters[$i] . "\n";
}
اشتباه رایج نهم: نادیده گرفتن استانداردهای کدنویسی
اگرچه نادیده گرفتن استانداردهای کدنویسی مستقیماً منجر به ایجاد خطا در کد PHP نمی شود اما هنوز هم یکی از مهم ترین مواردی است که باید در مورد بحث قرار گیرد.
نادیده گرفتن استانداردهای کدنویسی می تواند مشکلات زیادی را در یک پروژه ایجاد کند. از آنجایی که هر توسعه دهنده کار را به سبک خود انجام می دهد، پایبند نبودن به استاندارد های کدنویسی در بهترین حالت، منجر به ناسازگاری در کدها می شود اما در بدترین حالت، کد PHP تولید شده کار نمیکند یا پیمایش آن دشوار و گاهی تقریباً غیرممکن است. این کد ها قابلیت debug و بهینه سازی بسیار پایینی داشته و نگهداری و توسعه آنها کاری بسیار طاقت فرسا خواهد بود. در این صورت بهرهوری تیم توسعه دهنده نیز کاهش می یابد.
خوشبختانه برای نوشتن کد های php، پیشنهادات استانداردی (PSR) وجود دارد که از پنج گزینه زیر تشکیل شده اند:
- PSR-0: Autoloading Standard
- PSR-1: Basic Coding Standard
- PSR-2: Coding Style Guide
- PSR-3: Logger Interface
- PSR-4: Autoloader
PSR در اصل بر اساس اطلاعات دریافتی از تیم های پشتیبانی CMS ها و شناخته شده ترین پلتفرم های موجود در بازار ایجاد شده است. Zend، Drupal، Symfony، Joomla و دیگران به این استانداردها کمک کرده اند و اکنون از آنها پیروی می کنند. حتی PEAR که سال ها خود به عنوان یک استاندارد شناخته میشد، اکنون از PSR پیروی می نماید.
به طور کلی، پیروی از PSR ایده خوبی است مگر اینکه در پروژه خود دلیل قانعکنندهای برای انجام عملی برخلاف آن داشته باشید. تیم ها و پروژه های بیشتری با PSR مطابقت دارند و توسط اکثر توسعه دهندگان PHP به عنوان استاندارد شناخته می شود. در این صورت اعضای جدید یک تیم برنامهنویسی نیز با استاندارد کدنویسی آن مجموعه آشنایی و مطابقت خواهند داشت.
اشتباه رایج دهم: استفاده اشتباه از ()empty
برخی از توسعه دهندگان در نوشتن کد های php، تقریباً برای همه چیز از ()empty جهت برگرداندن مقدار true یا false استفاده میکنند. با این حال، مواردی وجود دارند که استفاده از ()empty می تواند منجر به سردرگمی شود.
ابتدا، بهتر است آرایه ها و نمونه های ArrayObject (آرایه ها را تقلید می کنند) بررسی شوند. با توجه به شباهت آنها، به راحتی می توان تصور کرد که آرایه ها و نمونه های ArrayObject یکسان رفتار می کنند. با این حال، استفاده از ()empty ثابت می کند که این یک فرض خطرناک است. به عنوان مثال، در PHP 5.0:
$array = [];
var_dump(empty($array)); // خروجی برابر با (true)
$array = new ArrayObject();
var_dump(empty($array)); // خروجی برابر با (false)
نتیجه اجرای کد بالا:
تصویر(6)
متاسفانه این رویکرد بسیار محبوب می باشد. به عنوان مثال، این راهی است که در Zend Framework 2 هنگام فراخوانی ()current در ()TableGateway::select، از آن استفاده می کند. توسعه دهنده می تواند به راحتی با چنین داده هایی قربانی این اشتباه شود. روش بهتر برای بررسی ساختارهای آرایه خالی، استفاده از ()count است:
// Note that this work in ALL versions of PHP (both pre and post 5.0):
$array = [];
var_dump(count($array)); // outputs int(0)
$array = new ArrayObject();
var_dump(count($array)); // outputs int(0)
از آنجایی که در نوشتن کد های php مقدار 0 را به false نسبت میدهد، میتوان از ()count در شرط if برای بررسی آرایههای خالی استفاده کرد. همچنین شایان ذکر می باشد که در PHP تابع ()count "پیچیدگی زمانی الگوریتم" روی آرایهها دارد که نشان می دهد انتخاب آن بسیار درست است. پیچیدگی زمانی الگوریتم، یک مفهوم در نظریه پیچیدگی محاسباتی و علوم کامپیوتر می باشد که مقدار زمان موردنیاز برای اجرا و متوقف شدن یک الگوریتم را مشخص می کند.
ترکیب ()empty و تابع کلاس ()get__ بسیار خطرناک است. در ادامه دو کلاس تعریف می شود و در هر دو، یک ویژگی تست می گردد.
ابتدا یک کلاس Regular تعریف می شود که شامل test به عنوان یک ویژگی عادی است:
class Regular
{
public $test = 'value';
}
سپس یک کلاس Magic تعریف می شود که از عملگر ()get__ برای دسترسی به ویژگی تست آن استفاده می کند:
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);
$magic = new Magic();
var_dump($magic->test);
نتیجه اجرای کد بالا در تصویر(7) قابل مشاهده است.
تصویر(7)
اما چه اتفاقی میافتد وقتی ()empty در هر یک از این بخش ها فراخوانی شود:
var_dump(empty($regular->test));
var_dump(empty($magic->test));
نتیجه اجرای کد، در تصویر(8) قابل مشاهده می باشد.
تصویر(8)
بنابراین، اگر به ()empty تکیه شود، ممکن است تصور شود که test در magic$ خالی می باشد، در حالی که روی 'value' تنظیم شده است. متاسفانه اگر کلاسی از تابع ()get__ برای بازیابی مقدار یک ویژگی استفاده کند، هیچ روشی برای بررسی خالی بودن یا نبودن آن مقدار وجود ندارد. در مقابل، اگر نیاز شود که به ویژگی ناموجود یک نمونه کلاس 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 در طول تاریخ 20 ساله خود به طور قابل توجهی تکامل یافته است. آشنایی با ظرافتهای آن تلاشی ارزشمند محسوب می شود. زیرا کمک میکند نرمافزاری که تولید میکنید مقیاسپذیرتر و قویتر باشد.
جهت جلوگیری از این خطاها، بهتر است که برنامهنویسان PHP از روشهای جدیدتر و امنتر برای دسترسی به پایگاه داده، خطایابی، بارگذاری فایل، تهیه خروجی، ایجاد متغیر، اجرای کد، ذخیره داده، تولید رشته تصادفی، فیلتر کردن ورودی و مقایسه مقادیر استفاده کنند. همچنین بهتر است که قبل از نوشتن یا تغییر کد PHP، آن را با استانداردهای برنامهنویسی PHP مطابقت دهند.