سه سال پیش، وسط دیباگ یک افزونه‌ی وردپرسی که فقط روی سرور مشتری رفتار عجیب داشت، به نقطه‌ای رسیدم که هیچ‌کدام از فرضیه‌هایم جواب نمی‌داد. کوئری‌ها را چک کرده بودم، هوک‌ها را لاگ گرفته بودم، حتی نسخه‌ی PHP را یک پله بالا برده بودم. مشکل جای دیگری بود: یک آرایه که بعد از array_filter ذخیره شده بود، بدون array_values به حلقه‌ی بعدی می‌رفت و foreach روی ایندکس صفر گیر می‌کرد. آن روز برایم روشن شد که آرایه‌ها در PHP فقط یک ساختار داده نیستند؛ آن‌ها زبان مادری PHP برای مدل‌سازی داده‌اند — از تنظیمات یک افزونه تا خروجی یک REST API و رندر یک قالب.

آرایه در PHP دقیقاً چیست؟

اگر با مفهوم پایه‌ی برنامه‌نویسی آشنا نیستید، اول آموزش PHP از صفر برای مبتدیان را بخوانید. اما تعریف فنی آرایه در PHP این است: یک نگاشت مرتب (ordered map) از کلیدها به مقادیر. این تعریف، دقیق‌تر از آن است که به نظر می‌رسد؛ چون PHP برخلاف زبان‌هایی مثل C یا Java، آرایه‌ی «هم‌جنس» ندارد. در PHP می‌توانید در یک آرایه‌ی واحد، رشته، عدد، آبجکت و حتی آرایه‌ی تودرتوی دیگر را کنار هم بگذارید.

این انعطاف، هم نعمت است هم درد. نعمت، چون مدل‌سازی داده‌های پیچیده را بدون تعریف کلاس ممکن می‌کند؛ درد، چون برنامه‌نویس را وسوسه می‌کند تا هر چیزی را در یک آرایه‌ی تو در تو بریزد و بعد کد، خوانایی‌اش را از دست بدهد. تجربه‌ام می‌گوید هرجا آرایه از سه سطح تودرتو رد شد، بهتر است سراغ یک آبجکت یا کلاس بروید. این همان تفکیکی است که در آموزش شی‌گرایی در PHP روی آن تأکید کرده‌ام.

نکته‌ی مهم دیگری که در بهترین روش‌های PHP مدرن بازش کرده‌ام: در PHP 8.x آرایه همچنان ساختار داده‌ی اول برای انتقال داده بین لایه‌های برنامه است، چون سبک‌تر از آبجکت است و سریالایز/دسریالایز در JSON و کش‌های transient طبیعی‌تر انجام می‌شود.

آرایه در PHP یک ظرف داده‌ی چندکاره است؛ هرچه کمتر آن را تو در تو کنید، کدتان عمر بیشتری می‌کند.

سه نوع آرایه که هر روز می‌بینم

در مستندات رسمی، PHP همه‌ی آرایه‌ها را «آرایه» می‌داند، ولی در عمل سه الگوی ذهنی داریم که دانستن تفاوتشان، خیلی از باگ‌ها را جلوگیری می‌کند.

۱) آرایه‌ی ایندکسی (Indexed)

کلیدها عدد صحیح متوالی از صفر هستند. این همان چیزی است که در زبان‌های دیگر «لیست» می‌نامند.

$fruits = ["apple", "banana", "cherry"];
echo $fruits[0]; // apple

وقتی این آرایه را با array_filter یا array_splice دستکاری می‌کنید، کلیدها پرش می‌کنند و دیگر از صفر شروع نمی‌شوند. این دقیقاً همان چیزی است که در مقدمه‌ی این مقاله به آن اشاره کردم.

۲) آرایه‌ی انجمنی (Associative)

کلیدها رشته هستند و برای نگاشت مستقیم استفاده می‌شوند. تنظیمات افزونه‌ها، پارامترهای کوئری و متادیتا در وردپرس معمولاً با همین ساختار ذخیره می‌شوند.

$config = [
    "cache"    => true,
    "timeout"  => 30,
    "endpoint" => "https://api.example.com",
];

۳) آرایه‌ی چندبعدی (Multidimensional)

آرایه‌ای که هر عضو آن، خودش یک آرایه است. خروجی REST API وردپرس، نتیجه‌ی کوئری‌های چندسطری و رکوردهای دیتابیس معمولاً این شکل را دارند.

$users = [
    ["id" => 1, "name" => "Ali",  "role" => "admin"],
    ["id" => 2, "name" => "Sara", "role" => "editor"],
];

وقتی با این ساختار کار می‌کنید، ابزارهایی مثل array_column و usort که در ادامه می‌گویم، تفاوت بین یک کد کوتاه و یک حلقه‌ی ۳۰ خطی می‌سازند.

نوعکلیدکاربرد رایج
ایندکسی0, 1, 2 …لیست‌ها، نتایج شمارشی
انجمنیرشته‌ایتنظیمات، متادیتا، پارامترها
چندبعدیترکیبیخروجی API، رکوردهای دیتابیس

توابع کلیدی که واقعاً استفاده می‌کنم

PHP بیش از ۸۰ تابع برای آرایه دارد؛ ولی در پروژه‌های واقعی، یک هسته‌ی کوچک ۹۰٪ کارها را انجام می‌دهد. این‌ها را به‌ترتیب دفعات استفاده می‌آورم.

افزودن و حذف

$stack = [];
array_push($stack, "a", "b");   // [a, b]
array_unshift($stack, "z");      // [z, a, b]
$last = array_pop($stack);       // b

نکته‌ای که خیلی از تازه‌کارها نمی‌دانند: array_push با یک عضو، از نظر سرعت تفاوت محسوسی با $stack[] = ... ندارد؛ ولی شکل دومی خواناتر است و در کدهای تولیدی ترجیح می‌دهم.

جستجو و استخراج

$user = ["id" => 1, "name" => "Ali"];

// بررسی وجود کلید
if (array_key_exists("email", $user)) { /* ... */ }

// استخراج یک ستون از آرایه چندبعدی
$names = array_column($users, "name");

تفاوت isset و array_key_exists را جدی بگیرید: اگر مقدار کلید null باشد، isset مقدار false برمی‌گرداند ولی array_key_exists مقدار true. این یک خط، چندین باگ خاموش در APIها ساخته است.

مرتب‌سازی

sort($numbers);              // مرتب‌سازی صعودی، حفظ نمی‌کند کلیدها را
asort($config);              // مرتب‌سازی با حفظ کلیدها
usort($rows, function ($a, $b) {
    return $a["score"] <=> $b["score"];
});

اپراتور سفینه‌ای (<=>) در PHP 7 معرفی شد و برای usort و uasort یک نعمت است؛ به‌جای اگر‌و‌اماهای -1, 0, 1، یک خط تمیز می‌نویسید.

تبدیل و ادغام

$merged   = array_merge($defaults, $options);       // ادغام سطحی
$flipped  = array_flip($map);                        // کلید ↔ مقدار
$unique   = array_unique($tags);                     // حذف تکراری
$chunked  = array_chunk($items, 10);                 // تکه‌تکه کردن

یک اشتباه رایج که در کدهای legacy زیاد می‌بینم: استفاده از array_merge برای آرایه‌های عددی وقتی قصد «جایگزینی» دارند. array_merge روی آرایه‌های ایندکسی، مقادیر را پشت هم می‌چسباند، نه جایگزین. برای جایگزینی آگاهانه، array_replace یا + را در نظر بگیرید.

تفاوت array_merge و array_replace را یک‌بار برای همیشه یاد بگیرید؛ هر دو در مستندات PHP یک صفحه فاصله دارند، ولی در بودجه‌ی اشکال‌زدایی، ده‌ها ساعت فاصله دارند.

الگوهای واقعی در پروژه‌های وردپرسی

آرایه در وردپرس همه‌جا هست: از $args در WP_Query تا گزینه‌های register_post_type و متادیتای کاربران. سه الگو که بارها در پروژه‌ها به‌کارم آمده را با هم مرور کنیم.

الگو ۱: آرایه‌ی تنظیمات با مقادیر پیش‌فرض

$defaults = [
    "limit" => 10,
    "order" => "DESC",
    "cache" => true,
];

$options = array_merge($defaults, array_intersect_key($user_input, $defaults));

این الگو، هم امنیت می‌آورد (فقط کلیدهای مجاز رد می‌شوند) و هم پیش‌فرض‌ها را تضمین می‌کند. در توسعه‌ی افزونه‌ها، این را یک بار در یک تابع کمکی بپیچید و همه‌جا استفاده کنید — همان اصلی که در ساختار فایل‌های یک افزونه استاندارد وردپرس هم روی جداسازی این نوع توابع تأکید کرده‌ام.

الگو ۲: Map و Reduce به‌جای حلقه

$prices = array_column($products, "price");
$total  = array_sum($prices);

$with_tax = array_map(fn ($p) => $p * 1.09, $prices);

$by_category = array_reduce($products, function ($carry, $item) {
    $carry[$item["category"]][] = $item;
    return $carry;
}, []);

تابع‌های array_map، array_filter و array_reduce هم خواناترند و هم معمولاً سریع‌تر از حلقه‌های دستی با foreach و append عمل می‌کنند. برای کارایی بیشتر، در بهینه‌سازی کدهای PHP چند الگوی مشابه را با اندازه‌گیری مقایسه کرده‌ام.

الگو ۳: آرایه‌ی انجمنی به‌عنوان دیکشنری

$labels = [
    "pending"   => "در انتظار",
    "completed" => "تکمیل‌شده",
    "failed"    => "ناموفق",
];

$status = "pending";
echo $labels[$status] ?? "نامشخص";

اپراتور ?? در PHP 7 به بعد، جایگزین تمیزتری برای الگوی قدیمی isset() ? ... : ... است و همان چیزی است که در مدیریت خطا در PHP روی آن تأکید کرده‌ام: نبودِ کلید یک خطای رایج است و باید پیش‌بینی شود، نه اینکه به Undefined index تبدیل شود.

کارایی، حافظه و copy-on-write

آرایه‌های بزرگ می‌توانند قاتل حافظه و زمان اجرا باشند، خصوصاً در کوئری‌های سنگین وردپرسی. سه نکته‌ی عملی که در پروژه‌ها به‌کارم آمده:

  • Copy-on-write: PHP آرایه‌ها را تا وقتی تغییر ندهید، به‌اشتراک می‌گذارد. به‌محض یک تغییر کوچک، کل آرایه کپی می‌شود. اگر آرایه‌ی چندهزار عضوی را در یک حلقه مرتب بازنویسی می‌کنید، این کپی‌ها هزینه‌ی سنگینی دارند.
  • حافظه‌ی هر عضو: برخلاف تصور، هر عنصر آرایه در PHP بخش قابل‌توجهی حافظه مصرف می‌کند (هر عنصر یک zval با overhead). وقتی یک آرایه از مرز چندصد هزار عضو رد می‌شود، جایگزین‌هایی مثل SplFixedArray یا پردازش دسته‌ای (chunking) را جدی بگیرید.
  • مرتب‌سازی گران است: usort روی آرایه‌ی بزرگ، O(n log n) است؛ ولی اگر مقایسه‌گر شما کار سنگین انجام دهد (مثلاً کوئری بزند)، همه‌چیز خراب می‌شود. مقایسه‌گر را همیشه خالص و سریع نگه دارید.

در یک پروژه‌ی فروشگاهی، جایی که آرایه‌ی محصولات مستقیماً از $wpdb->get_results می‌آمد، با تبدیل آرایه‌ی آبجکت‌ها به آرایه‌ی انجمنی و کاهش عمق تودرتویی، مصرف حافظه‌ی یک گزارش ماهانه حدود یک‌سوم پایین آمد. عدد دقیق مهم نیست؛ مهم این است که وقتی به سقف حافظه نزدیک می‌شوید، اولین مظنون، ساختار آرایه است — همان درسی که در بهینه‌سازی کوئری‌های وردپرس با کدنویسی هم روی آن تأکید کرده‌ام.

اشتباهاتی که بارها دیده‌ام

فهرست کوتاهی از اشتباهاتی که در کدهای legacy و پروژه‌های تازه‌کار به‌وفور دیده‌ام:

  • مقایسه با == به‌جای ===: in_array("1", [1, 2, 3]) با مقایسه‌ی شل، true برمی‌گرداند. اگر با داده‌ی کاربر کار می‌کنید، همیشه پارامتر سوم را true بگذارید.
  • ذخیره‌ی آرایه در متادیتا بدون سریالایز آگاهانه: update_post_meta آرایه را خودش سریالایز می‌کند، ولی اگر بعداً بخواهید با LIKE روی دیتابیس جستجو کنید، غافلگیر می‌شوید.
  • نبودِ array_values بعد از فیلتر: همان باگی که در مقدمه گفتم. هرجا آرایه‌ی ایندکسی را با array_filter دستکاری می‌کنید، اگر ترتیب ایندکس برایتان مهم است، array_values را فراموش نکنید.
  • استفاده از array_shift در حلقه: این تابع، آرایه را هر بار دوباره ایندکس می‌کند و در حلقه‌ی بزرگ، کارایی فاجعه‌بار است. برای صف، SplQueue یا array_pop به‌همراه array_reverse گزینه‌های بهتری هستند.
  • نبودِ isset قبل از خواندن: مستقیم رفتن سراغ $arr["key"] وقتی کلید ممکن است وجود نداشته باشد، هم Notice می‌سازد هم باگ خاموش. همان نکته‌ای که در اشتباهات رایج در کدنویسی وردپرس مفصل بازش کرده‌ام.
هر بار که یک Notice: Undefined index را با @ خفه کرده‌اید، یک باگ خاموش را در پروژه‌ی آینده کاشته‌اید.

آرایه‌ها و امنیت ورودی

آرایه‌های ورودی، دروازه‌ی ورود بخش بزرگی از حملات هستند. سه نکته‌ی مهم:

  1. هرگز به آرایه‌ی خام $_POST یا $_GET اعتماد نکنید. قبل از هر استفاده، هم ساختار و هم مقدار را با توابعی مثل sanitize_text_field، absint، wp_kses_post تمیز کنید. اصول کلی را در امنیت در PHP با مثال آورده‌ام.
  2. آرایه‌ی تو در تو، نقطه‌ی تزریق: اگر آرایه‌ی خام ورودی را بدون پیمایش بازگشتی ذخیره می‌کنید، مهاجم می‌تواند در عمق چند‌سطحی، مقدار مخرب بگذارد. برای همین، در پروژه‌های حساس، تابع بازگشتی تمیزکاری می‌نویسم یا از map_deep وردپرس استفاده می‌کنم.
  3. ذخیره‌سازی دیتابیس: وقتی آرایه را با $wpdb->insert یا $wpdb->prepare ذخیره می‌کنید، هر عضو را جداگانه آماده کنید. هرگز آرایه‌ی سریالایز‌شده را داخل کوئری نچپانید.

PHP 8 و تغییرات مهم آرایه‌ها

نسخه‌ی ۸ چند تغییر کاربردی برای آرایه‌ها آورد که در پروژه‌های جدید از آن‌ها استفاده می‌کنم:

  • spread با کلید رشته‌ای: از PHP 8.1 می‌توانید آرایه‌های دارای کلید رشته‌ای را با ... در آرایه‌ی جدید باز کنید. قبلاً فقط کلیدهای عددی مجاز بودند.
  • Named arguments: هنگام صدا زدن توابعی مثل array_merge که پارامترهای زیادی دارند، می‌توانید نام‌شان را صریح بدهید. خوانایی به‌شدت بالا می‌رود.
  • تابع array_is_list(): دقیقاً همان چیزی که سال‌ها می‌خواستیم. تشخیص می‌دهد یک آرایه، «لیست خالص» است یا نه — بدون الگوی دستی array_keys($arr) === range(0, count($arr) - 1).

اختلاف نسخه‌ها را در تفاوت PHP 7 و PHP 8 با جزئیات بیشتری مقایسه کرده‌ام؛ اگر هنوز روی PHP 7 مانده‌اید، مهاجرت را جدی بگیرید، چون ابزارهای جدید آرایه، تفاوت محسوسی در خوانایی و کارایی می‌سازند.

دیدگاه مهندسی پیشرفته

برای توسعه‌دهندگان ارشد و معماران پلتفرم، آرایه در PHP سه هشدار جدی دارد که در مقیاس بزرگ‌تر (چند میلیون رکورد، چندصد درخواست در ثانیه) خودشان را نشان می‌دهند. اول: نگاشت مرتب آرایه در PHP، پیاده‌سازی داخلی پیچیده‌ای دارد که در کلیدهای رشته‌ای با نرخ برخورد بالا، افت عملکرد محسوس می‌سازد؛ در سیستم‌هایی که دیکشنری بزرگ می‌سازید، به ساختارهای تخصصی مثل Ds\Map از افزونه‌ی php-ds فکر کنید. دوم: آرایه در PHP یک مقدار (value) است نه مرجع (reference)؛ این یعنی در فراخوانی‌های عمیق تابعی، کپی‌های ناخواسته هزینه‌ی حافظه و CPU می‌سازند. برای ساختارهای حجیم، یا از Generator استفاده کنید یا با پارامتر & به‌صورت مرجع پاس بدهید — ولی هر دو، بدهی خوانایی دارند و باید آگاهانه انتخاب شوند. سوم: کوئری‌های IN (...) با آرایه‌های بزرگ، دیوار مخفی مقیاس‌پذیری‌اند؛ جای مناسب‌شان JOIN یا جدول موقت است، نه آرایه‌ی چندهزار عضوی. اگر پروژه‌تان مرز وب‌سایت معمولی را رد کرده، معماری داده را در PHP در وردپرس، از مبتدی تا حرفه‌ای بازبینی کنید — این مرحله، جایی است که ابزارهایی مثل PDO و آبجکت‌گرایی وارد می‌شوند و در آموزش PDO در PHP نمونه‌های کاربردشان آمده است.

جمع‌بندی

آرایه در PHP فقط یک ساختار داده نیست؛ یک ابزار ارتباطی است بین لایه‌های برنامه — از فرم کاربر تا دیتابیس و تا پاسخ API. هرچه در کار با آن دقیق‌تر باشید، کدتان کوتاه‌تر، خواناتر و امن‌تر می‌شود. سه نکته‌ی طلایی که در این مقاله به آن‌ها رسیدیم: (۱) تا حد امکان از تودرتویی بیش از دو سطح پرهیز کنید؛ (۲) بعد از هر فیلتر یا splice، وجود array_values را آگاهانه بررسی کنید؛ (۳) هرگز به آرایه‌ی خام ورودی اعتماد نکنید.

اگر همین امشب یک کار کوچک بکنید، سود بیشتری از ده‌ها ساعت مطالعه دارد: در پروژه‌ی فعلی‌تان، جای‌هایی که آرایه‌ی ایندکسی را با array_filter دستکاری می‌کنید بگردید و برای هر مورد بپرسید «آیا بعدش به ایندکس صفر تکیه می‌کنم؟» — اگر پاسخ بله است، همان‌جا باگ خاموشی نشسته که فقط منتظر شرایط خاص است تا سر بلند کند. تجربه‌ی خودتان از چنین باگ‌هایی را در دیدگاه‌ها بنویسید؛ مخصوصاً اگر راه‌حل متفاوتی برای پیمایش آرایه‌های بزرگ به‌کار می‌برید که می‌تواند برای خواننده‌ی بعدی مفید باشد. 🧩