إذا كنت قد قمت بالفعل بلصق جزء من التعليمات البرمجية "WordPress في ووردبريس" وانتهى الأمر بصفحة فارغة أو خطأ فادحغالباً ما تكون المشكلة واحدة: إما أنك قمت بتحميل ووردبريس في المكان الخطأ، أو قمت بتحميله مرتين. والسبب وراء هذه المشكلة واضح تماماً: تنفيذ كود ووردبريس (الاستعلامات، إنشاء المنشورات، قراءة الخيارات، إرسال رسائل البريد الإلكتروني، إلخ) من سياق ليس "شاشة ووردبريس التقليدية".

في أبريل 2026، مع ووردبريس 6.9.4 و PHP 8.1+، نادرًا ما تتضمن الطريقة "الفعالة" لاستخدام ووردبريس داخل ووردبريس تضمين wp-load.php يدويًا. لديك خيارات أفضل: WP-CLI، أو نقطة نهاية REST، أو مهمة cron، أو إضافة mu، أو سكربت. ألبس الحذاء clean، الذي يقوم بتحميل WordPress مرة واحدة فقط ويستخدم واجهات برمجة التطبيقات الأصلية.

المشكلة / الحاجة

تريد تشغيل إجراءات ووردبريس من داخل ووردبريس، ولكن ليس بالضرورة "على صفحة":

  • استيراد البيانات من ملف CSV في الخلفية،
  • تنظيف البيانات الوصفية
  • إنشاء الصور،
  • نشر المسودات على دفعات،
  • كشف خدمة داخلية مصغرة (على سبيل المثال، إعادة حساب مخبأ طلب)،
  • تشغيل برنامج نصي لمرة واحدة دون فتح المسؤول.

في النهاية، ستعرف كيفية اختيار الآلية المناسبة (WP-CLI، REST، Cron، Bootstrap script)، وسيكون لديك رمز قابل للنسخ واللصق يقوم بما يلي:

  • يتم تحميل ووردبريس بشكل صحيح (إذا لزم الأمر).
  • يحمي التنفيذ (القدرات + الرمز المميز / nonce)،
  • يتجنب التحميل المزدوج ومخاطر الأداء،
  • يعمل على ووردبريس 6.9.4 / PHP 8.1+.

ملخص سريع

  • نتجنب تضمين ووردبريس "بشكل عشوائي" (الأسلوب الكلاسيكي) require wp-load.php (من الإنترنت).
  • نحن نعطي الأولوية WP-CLI للوظائف العرضية/الإدارية (الأكثر موثوقية).
  • نقترح نقطة نهاية REST آمنة لتفعيل إجراء من الخارج (أو من مُنشئ عبر زر).
  • نضيف مهمة WP-Cron لتحويل عملية المعالجة والحد من حالات انتهاء المهلة.
  • نحن نقدم مو البرنامج المساعد تماما (نسخ ولصق) + أمر اختياري لـ WP-CLI.

متى يستخدم هذا الحل

  • أنت بحاجة إلى "أداة داخلية" (إعادة بناء ذاكرة التخزين المؤقت، والمزامنة، والاستيراد، والصيانة).
  • لا ترغب في الاعتماد على إضافة مقتطفات هشة (لقد رأيت في كثير من الأحيان مواقع تتعطل بعد تحديث قالب فرعي يقوم بالكتابة فوقها). functions.php).
  • تحتاج إلى تشغيل عملية من Elementor/Divi/Avada عبر زر أو طلب. AJAX/استراحة.
  • أنت بحاجة إلى علاج غير متزامن (بداية سريعة، وعلاج أطول بعد ذلك).

متى لا يجب استخدام هذا الحل

  • إذا كنت ترغب في عرض محتوى على صفحة، فاستخدم كتلة أو رمزًا مختصرًا أو أداة أو نمطًا بدلاً من ذلك. تحميل ووردبريس "داخل ووردبريس" غير منطقي في هذه الحالة.
  • إذا كنت ترغب في إجراء عملية استيراد ضخمة (أكثر من 100 ألف سطر) على استضافة مشتركة: قد يكون WP-Cron + REST كافيًا، ولكن الخيار الجيد حقًا غالبًا ما يكون WP-CLI (أو عامل خارجي).
  • إذا كنت بحاجة إلى تعديل (تنسيق، تصميم): استخدم الأدوات الأصلية (محرر الكتل) أو وحدات البناء. لا تحوّل الحاجة إلى محتوى إلى حاجة برمجية.
  • إذا كنت تبحث عن واجهة برمجة تطبيقات عامة، فقد يكون استخدام نقطة نهاية REST مناسبًا، ولكن ستحتاج إلى إدارة المصادقة والحصص والسجلات، وأحيانًا جدار حماية تطبيقات الويب. وإلا، فاستخدم بوابة واجهة برمجة تطبيقات مخصصة.

المتطلبات الأساسية / قبل البدء

  • WordPress 6.9.4 (أو أحدث) و PHP 8.1+.
  • بيئة تجريبية ونسخة احتياطية. اختبار برنامج الصيانة على بيئة الإنتاج بدون لقطة هو أفضل طريقة لقضاء أمسيتك في الاستعادة.
  • الوصول إلى WP-CLI إن أمكن (غالباً ما يكون متاحاً حتى على الاستضافة المشتركة).
  • محرر أكواد وإمكانية الوصول إلى FTP/SSH لتحميل إضافة mu.

موارد مفيدة (للاحتفاظ بها في متناول اليد):

النهج الساذج (ولماذا يجب تجنبه)

المقطع الذي ما زلت أراه متداولاً (والذي "يعمل" حتى اليوم الذي يتوقف فيه عن العمل):

<?php
// ❌ Exemple à éviter : script accessible via le web + chargement WordPress à la main
require __DIR__ . '/wp-load.php';

$posts = get_posts(['numberposts' => 10]);
foreach ($posts as $p) {
    echo esc_html($p->post_title) . "<br>";
}

ما الخطأ عملياً؟

  • أمن إذا كان هذا الملف متاحًا للعامة، فإنك تُعرّض نفسك لخطر الهجوم. ينسى الكثيرون إضافة المصادقة.
  • هاملت قد يكون تحميل تثبيت ووردبريس بالكامل لإجراء بسيط مكلفًا، خاصة إذا كانت هناك إضافات ثقيلة قيد التهيئة.
  • التمهيد المزدوج : إذا قمت بلصق هذا الكود في سياق تم فيه تحميل ووردبريس بالفعل (على سبيل المثال: functions.php)، مما يؤدي إلى ظهور أخطاء مثل "لا يمكن إعادة التصريح ..." أو سلوك غريب.
  • الدورية : هذا الملف "الجانبي" لا يتم حفظ نسخ منه وفقًا لممارساتك المعتادة، وينتهي به الأمر إلى النسيان.

النهج الصحيح - دليل خطوة بخطوة

سنقوم بتطبيق "مجموعة" صيانة نظيفة، على شكل مو البرنامج المساعد (يتم تحميلها تلقائيًا)، والتي توفر ما يلي:

  • نقطة نهاية REST مصادق عليه لتفعيل إجراء ما،
  • محفز غير متزامن عبر WP-Cron لتجنب انقطاع الاتصال،
  • وضع "التجربة الجافة" للاختبار دون تعديل القاعدة،
  • أمر WP-CLI (اختياري) إذا كان WP-CLI متاحًا.

الخطوة 1 - إنشاء إضافة mu

أنشئ المجلد (إذا لم يكن موجودًا بالفعل) wp-content/mu-plugins/ثم أضف ملفًا: wp-content/mu-plugins/bpcab-maintenance-kit.php.

لماذا إضافة mu-plugin؟ لأنها تُحمّل قبل الإضافات الكلاسيكية، ولا تعتمد على القالب، وتتجنب النمط السيئ المتمثل في "وضع كل شيء في functions.php".

الخطوة 2 - تحديد إجراء صيانة فعلي

مثال ملموس: إعادة حساب بيانات ميتا reading_time فيما يخص أحدث المقالات (مفيد للقوالب، أو أدوات إنشاء الصفحات، أو الأقسام التي تعرض "مدة القراءة بالدقائق X"). كثيراً ما رأيت هذا الحقل يُحسب عند عرض الصفحة، مما يتطلب استعلاماً بالإضافة إلى عملية حسابية لكل صفحة يتم عرضها.

لذا سنفعل:

  • مواقع مستهدفة post نُشر،
  • احسب وقت القراءة بناءً على عدد الكلمات،
  • اكتب النتيجة في بيانات تعريف المنشور.
  • قم بتقييد معالجة الدفعات (الترقيم) لتجنب التحميل الزائد على الذاكرة.

الخطوة 3 - كشف نقطة نهاية REST آمنة

نحن ننشئ طريقًا /wp-json/bpcab/v1/maintenance/reading-time من :

  • يتطلب مستخدمًا قادرًا (manage_options),
  • يتطلب ذلك رمز REST (للمكالمات من المسؤول) أو رمز HMAC (للمكالمات من الخادم).
  • يقوم بتشغيل مهمة WP-Cron في الخلفية.

الخطوة 4 - إضافة WP-Cron للمعالجة غير المتزامنة

يجب أن تستجيب نقطة النهاية بسرعة. تتم معالجة الدفعات عبر خطاف cron، والذي يُعاد تشغيله حتى اكتمال العملية.

الخطوة 5 — (اختياري) إضافة أمر WP-CLI

لا تزال أداة WP-CLI هي الأفضل للاستيراد والصيانة. فهي تتجنب انقطاعات HTTP، وتوفر سجلات النظام، ويمكن تشغيلها عبر SSH.

الرمز الكامل

انسخ هذا الملف والصقه كما هو في wp-content/mu-plugins/bpcab-maintenance-kit.php.

<?php
/**
 * Plugin Name: BPCAB Maintenance Kit (MU)
 * Description: Endpoint REST + WP-Cron (+ option WP-CLI) pour exécuter des tâches de maintenance propres (WP 6.9.4+, PHP 8.1+).
 * Version: 1.0.0
 * Author: BPCAB
 *
 * Ce fichier doit être placé dans wp-content/mu-plugins/
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
	exit;
}

final class BPCAB_Maintenance_Kit {

	private const REST_NAMESPACE = 'bpcab/v1';
	private const CRON_HOOK_READING_TIME = 'bpcab_mk_cron_reading_time';
	private const OPTION_SECRET = 'bpcab_mk_secret';
	private const DEFAULT_BATCH_SIZE = 50;

	public static function init(): void {
		add_action('rest_api_init', [__CLASS__, 'register_routes']);
		add_action(self::CRON_HOOK_READING_TIME, [__CLASS__, 'cron_run_reading_time'], 10, 1);

		// Initialisation du secret au besoin (une seule fois).
		add_action('admin_init', [__CLASS__, 'maybe_generate_secret']);

		// Optionnel : commande WP-CLI si WP_CLI est présent.
		if (defined('WP_CLI') && WP_CLI) {
			self::register_wp_cli();
		}
	}

	/**
	 * Génère un secret stocké en option, utilisé pour signer des requêtes serveur-à-serveur.
	 * On le fait côté admin_init pour éviter de créer des options sur le front.
	 */
	public static function maybe_generate_secret(): void {
		if (!current_user_can('manage_options')) {
			return;
		}

		$secret = get_option(self::OPTION_SECRET);
		if (is_string($secret) && strlen($secret) >= 32) {
			return;
		}

		// wp_generate_password() est adapté pour un secret long.
		$secret = wp_generate_password(64, true, true);
		update_option(self::OPTION_SECRET, $secret, true);
	}

	public static function register_routes(): void {
		register_rest_route(
			self::REST_NAMESPACE,
			'/maintenance/reading-time',
			[
				[
					'methods'             => 'POST',
					'callback'            => [__CLASS__, 'rest_trigger_reading_time'],
					'permission_callback' => [__CLASS__, 'rest_permission_callback'],
					'args'                => [
						'batch_size' => [
							'type'              => 'integer',
							'required'          => false,
							'default'           => self::DEFAULT_BATCH_SIZE,
							'sanitize_callback' => 'absint',
						],
						'dry_run' => [
							'type'              => 'boolean',
							'required'          => false,
							'default'           => false,
							'sanitize_callback' => [__CLASS__, 'sanitize_boolean'],
						],
						'cursor' => [
							'type'              => 'integer',
							'required'          => false,
							'default'           => 0,
							'sanitize_callback' => 'absint',
						],
					],
				],
			]
		);
	}

	/**
	 * Permission REST :
	 * - Cas 1 : appel depuis un utilisateur connecté (admin) => capability manage_options + nonce REST standard.
	 * - Cas 2 : appel serveur-à-serveur => HMAC dans en-tête X-BPCAB-Signature + X-BPCAB-Timestamp.
	 *
	 * Note : en REST, WordPress gère le nonce via l'en-tête X-WP-Nonce pour les utilisateurs connectés.
	 */
	public static function rest_permission_callback(WP_REST_Request $request): bool|WP_Error {
		// Utilisateur connecté : on exige une capability forte.
		if (is_user_logged_in()) {
			if (!current_user_can('manage_options')) {
				return new WP_Error('forbidden', 'Droits insuffisants.', ['status' => 403]);
			}
			return true;
		}

		// Sinon : signature HMAC.
		$timestamp = $request->get_header('x-bpcab-timestamp');
		$signature = $request->get_header('x-bpcab-signature');

		if (!is_string($timestamp) || !is_string($signature) || $timestamp === '' || $signature === '') {
			return new WP_Error('unauthorized', 'Signature manquante.', ['status' => 401]);
		}

		if (!ctype_digit($timestamp)) {
			return new WP_Error('unauthorized', 'Timestamp invalide.', ['status' => 401]);
		}

		$ts = (int) $timestamp;

		// Fenêtre anti-rejeu : 5 minutes.
		if (abs(time() - $ts) > 300) {
			return new WP_Error('unauthorized', 'Timestamp expiré.', ['status' => 401]);
		}

		$secret = get_option(self::OPTION_SECRET);
		if (!is_string($secret) || strlen($secret) < 32) {
			return new WP_Error('server_error', 'Secret non initialisé.', ['status' => 500]);
		}

		// Message signé : méthode + route + timestamp + body brut.
		$raw_body = $request->get_body();
		if (!is_string($raw_body)) {
			$raw_body = '';
		}

		$message = implode('|', [
			'POST',
			'/wp-json/' . self::REST_NAMESPACE . '/maintenance/reading-time',
			(string) $ts,
			$raw_body,
		]);

		$expected = hash_hmac('sha256', $message, $secret);

		// Comparaison en temps constant.
		if (!hash_equals($expected, $signature)) {
			return new WP_Error('unauthorized', 'Signature invalide.', ['status' => 401]);
		}

		return true;
	}

	public static function rest_trigger_reading_time(WP_REST_Request $request): WP_REST_Response|WP_Error {
		$batch_size = max(1, min(200, (int) $request->get_param('batch_size')));
		$dry_run    = (bool) $request->get_param('dry_run');
		$cursor     = max(0, (int) $request->get_param('cursor'));

		$payload = [
			'batch_size' => $batch_size,
			'dry_run'    => $dry_run,
			'cursor'     => $cursor,
		];

		// Planifie immédiatement un event unique.
		// On évite wp_schedule_event (récurrent) : ici on veut un job ponctuel, relancé si besoin.
		wp_schedule_single_event(time() + 1, self::CRON_HOOK_READING_TIME, [$payload]);

		return new WP_REST_Response(
			[
				'status'     => 'scheduled',
				'payload'    => $payload,
				'next_step'  => 'WP-Cron va traiter le batch et replanifier si nécessaire.',
				'tip'        => 'Si WP-Cron est désactivé, exécutez wp cron event run --due-now via WP-CLI.',
			],
			202
		);
	}

	/**
	 * Traitement cron : calcule reading_time pour un batch, puis replanifie si on n'a pas fini.
	 *
	 * @param array $payload
	 */
	public static function cron_run_reading_time(array $payload): void {
		$batch_size = isset($payload['batch_size']) ? (int) $payload['batch_size'] : self::DEFAULT_BATCH_SIZE;
		$batch_size = max(1, min(200, $batch_size));

		$dry_run = !empty($payload['dry_run']);
		$cursor  = isset($payload['cursor']) ? (int) $payload['cursor'] : 0;
		$cursor  = max(0, $cursor);

		$query = new WP_Query([
			'post_type'              => 'post',
			'post_status'            => 'publish',
			'posts_per_page'         => $batch_size,
			'orderby'                => 'ID',
			'order'                  => 'ASC',
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'ignore_sticky_posts'    => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'paged'                  => 1,
			// Curseur simple : on reprend après un ID donné.
			'date_query'             => [],
			'meta_query'             => [],
			'tax_query'              => [],
			'author__in'             => [],
			'post__not_in'           => [],
			'suppress_filters'       => false,
		]);

		// Filtrer par ID > cursor sans requête custom : on utilise un filtre posts_where.
		// On l'ajoute juste le temps de CETTE requête.
		$filter = static function (string $where) use ($cursor): string {
			global $wpdb;
			if ($cursor > 0) {
				$where .= $wpdb->prepare(" AND {$wpdb->posts}.ID > %d", $cursor);
			}
			return $where;
		};

		add_filter('posts_where', $filter, 10, 1);
		$query->get_posts();
		remove_filter('posts_where', $filter, 10);

		$post_ids = $query->posts;
		if (!is_array($post_ids) || empty($post_ids)) {
			// Rien à faire : fin du job.
			return;
		}

		$last_id = $cursor;

		foreach ($post_ids as $post_id) {
			$post_id = (int) $post_id;
			$last_id = max($last_id, $post_id);

			$content = get_post_field('post_content', $post_id);
			if (!is_string($content)) {
				$content = '';
			}

			// Nettoyage : on retire les shortcodes et balises pour compter des mots “réels”.
			$text = wp_strip_all_tags(strip_shortcodes($content));
			$text = trim(preg_replace('/s+/u', ' ', $text) ?? '');

			$word_count = 0;
			if ($text !== '') {
				// Compte approximatif : suffisant pour un reading time.
				$words = preg_split('/s+/u', $text);
				$word_count = is_array($words) ? count($words) : 0;
			}

			// Hypothèse : 200 mots/minute (ajustez selon votre langue/ton).
			$minutes = (int) max(1, (int) ceil($word_count / 200));

			if (!$dry_run) {
				update_post_meta($post_id, 'reading_time', $minutes);
				update_post_meta($post_id, 'reading_time_words', $word_count);
			}
		}

		// Replanifie le batch suivant.
		$next_payload = [
			'batch_size' => $batch_size,
			'dry_run'    => $dry_run,
			'cursor'     => $last_id,
		];

		wp_schedule_single_event(time() + 2, self::CRON_HOOK_READING_TIME, [$next_payload]);
	}

	public static function sanitize_boolean(mixed $value): bool {
		// Accepte true/false, "true"/"false", 1/0, "1"/"0".
		if (is_bool($value)) {
			return $value;
		}
		if (is_numeric($value)) {
			return ((int) $value) === 1;
		}
		if (is_string($value)) {
			$v = strtolower(trim($value));
			return in_array($v, ['1', 'true', 'yes', 'on'], true);
		}
		return false;
	}

	private static function register_wp_cli(): void {
		WP_CLI::add_command('bpcab reading-time', function(array $args, array $assoc_args) {
			$batch_size = isset($assoc_args['batch_size']) ? (int) $assoc_args['batch_size'] : self::DEFAULT_BATCH_SIZE;
			$batch_size = max(1, min(500, $batch_size));

			$dry_run = !empty($assoc_args['dry-run']);

			$cursor = 0;
			$total  = 0;

			WP_CLI::log('Calcul reading_time (batch=' . $batch_size . ', dry-run=' . ($dry_run ? 'oui' : 'non') . ')');

			while (true) {
				$payload = [
					'batch_size' => $batch_size,
					'dry_run'    => $dry_run,
					'cursor'     => $cursor,
				];

				// On exécute la même logique que le cron, mais en synchrone.
				$before = $cursor;
				self::cron_run_reading_time($payload);

				// On doit deviner si ça a avancé : on relit le dernier ID du batch en refaisant une requête légère.
				$ids = self::get_post_ids_after_cursor($cursor, $batch_size);
				if (empty($ids)) {
					break;
				}
				$cursor = max($ids);
				if ($cursor === $before) {
					break;
				}

				$total += count($ids);
				WP_CLI::log('... traité ~' . $total . ' posts (cursor=' . $cursor . ')');
			}

			WP_CLI::success('Terminé.');
		});
	}

	/**
	 * Helper WP-CLI : récupère des IDs après un curseur sans recalcul.
	 */
	private static function get_post_ids_after_cursor(int $cursor, int $limit): array {
		$q = new WP_Query([
			'post_type'              => 'post',
			'post_status'            => 'publish',
			'posts_per_page'         => $limit,
			'orderby'                => 'ID',
			'order'                  => 'ASC',
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		]);

		$filter = static function (string $where) use ($cursor): string {
			global $wpdb;
			if ($cursor > 0) {
				$where .= $wpdb->prepare(" AND {$wpdb->posts}.ID > %d", $cursor);
			}
			return $where;
		};

		add_filter('posts_where', $filter, 10, 1);
		$q->get_posts();
		remove_filter('posts_where', $filter, 10);

		return is_array($q->posts) ? array_map('intval', $q->posts) : [];
	}
}

BPCAB_Maintenance_Kit::init();

شرح الكود

ما يفعله هذا الملحق (mu-plugin) باللغة الإنجليزية البسيطة

  • يضيف مسار REST يقوم بجدولة مهمة.
  • تقوم المهمة بالحساب reading_time يقوم النظام بتخزين النتيجة في البيانات الوصفية على دفعات، ثم يعيد جدولة نفسه طالما بقيت هناك منشورات.
  • يتجنب هذا النظام حالات انتهاء مهلة HTTP عن طريق نقل "الجزء الأكبر" من العمل إلى WP-Cron.
  • يوفر توقيع HMAC لتشغيل المهمة بدون جلسة WordPress (حالة الخادم إلى الخادم).

لماذا استخدام REST + Cron (بدلاً من استخدام سكربت PHP مباشر)؟

إليك ما يحدث في الخفاء على العديد من المواقع الإلكترونية: يتم استدعاء نص برمجي مباشر عبر الإنترنت، فيقوم بتحميل ووردبريس، لكن العملية تتوقف (بسبب انتهاء المهلة أو تجاوز حد الذاكرة) في منتصفها. وبالتالي، يصبح لديك حالة معدلة جزئيًا، وأحيانًا لا يوجد أي أثر قابل للاستخدام على الإطلاق.

يتولى REST + Cron إدارة عبء العمل. تستجيب نقطة النهاية بسرعة (202)، ويتم المعالجة على دفعات. وهذا أكثر موثوقية، خاصة مع المكونات الإضافية أو أدوات البناء التي تستهلك موارد كثيرة.

الأمان: القدرات، والقيمة العشوائية، ورمز مصادقة الرسائل (HMAC)

  • المستخدم المسجل هذا مطلوب manage_optionsبالنسبة لطلب من لوحة التحكم، يقوم ووردبريس بمعالجة قيمة REST nonce عبر رأس الطلب. X-WP-Nonce (جانب إدارة جافا سكريبت الكلاسيكي).
  • من خادم إلى خادم نستخدم توقيع HMAC (hash_hmac) على الطريقة + المسار + الطابع الزمني + نص الرسالة. تحدد نافذة الخمس دقائق عدد مرات إعادة التشغيل.

لماذا لا نستخدم "مفتاحًا سريًا في عنوان URL"؟ لأن عناوين URL تُسجّل في سجلات المواقع، والتحليلات، ومواقع الإحالة. لقد رأيتُ مفاتيح مكشوفة بهذه الطريقة على مواقع ويب تتم صيانتها بشكل جيد.

الأداء: تم تحسين WP_Query

  • 'fields' => 'ids' يتجنب تحميل كائنات المنشور كاملة.
  • 'no_found_rows' => true يتجنب استخدام التصفح في لغة SQL.
  • update_post_meta_cache et update_post_term_cache à false تقليل استهلاك الذاكرة.
  • يتم جمع 50 دفعة افتراضيًا، بحد أقصى 200 في REST (500 في CLI).

لماذا "شريط تمرير" قائم على الهوية؟

نستأنف العملية بعد آخر مُعرّف تمت معالجته. إنها عملية بسيطة ومستقرة، وتتجنب إزاحات SQL (التي تصبح مكلفة مع الأحجام الكبيرة). عامل التصفية posts_where يضيف الشرط ID > cursor فقط بناءً على الطلب.

نعم، إنه فلتر. ونعم، عليك إزالته فورًا بعد استخدامه، وإلا ستؤثر سلبًا على الاستعلامات الأخرى. هذا خطأ شائع عند البدء باستخدام فلاتر SQL.

المتغيرات وحالات الاستخدام

الخيار 1 - التشغيل من نص برمجي خارجي (curl) باستخدام HMAC

إذا كان لديك خادم تكامل، أو مهمة cron للنظام، أو أداة نشر، فيمكنك تشغيل نقطة نهاية WP بدون جلسة.

مثال على استخدام Bash (تحتاج إلى استرداد السر المخزن في الخيار) bpcab_mk_secret (عبر WP-CLI مرة واحدة):

# Récupérer le secret (à faire en admin / SSH)
wp option get bpcab_mk_secret

# Déclencher le job (exemple)
URL="https://example.com/wp-json/bpcab/v1/maintenance/reading-time"
TS="$(date +%s)"
BODY='{"batch_size":80,"dry_run":false,"cursor":0}'

# Message signé : méthode|route|timestamp|body
MSG="POST|/wp-json/bpcab/v1/maintenance/reading-time|${TS}|${BODY}"

# Signature HMAC SHA256 (nécessite openssl)
SECRET="COLLEZ_ICI_VOTRE_SECRET"
SIG="$(printf "%s" "$MSG" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -sS -X POST "$URL" 
  -H "Content-Type: application/json" 
  -H "X-BPCAB-Timestamp: $TS" 
  -H "X-BPCAB-Signature: $SIG" 
  --data "$BODY"

الخيار الثاني - الحساب في وقت النشر (بدلاً من وظيفة عالمية)

إذا كان شرطك هو "التحديث الدائم" وكنت تنشر بشكل غير متكرر، فاحسب التحديث عند حفظ المقالة. غالبًا ما يكون هذا أكثر كفاءة من المعالجة الدفعية.

<?php
// À mettre dans un plugin classique ou mu-plugin.
add_action('save_post_post', function(int $post_id, WP_Post $post, bool $update) {
	// ✅ Sécurité : éviter autosave/révisions.
	if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
		return;
	}

	// ✅ Permissions : vérifier l'édition.
	if (!current_user_can('edit_post', $post_id)) {
		return;
	}

	$content = (string) $post->post_content;
	$text = wp_strip_all_tags(strip_shortcodes($content));
	$text = trim(preg_replace('/s+/u', ' ', $text) ?? '');

	$words = $text !== '' ? preg_split('/s+/u', $text) : [];
	$count = is_array($words) ? count($words) : 0;
	$minutes = (int) max(1, (int) ceil($count / 200));

	update_post_meta($post_id, 'reading_time', $minutes);
	update_post_meta($post_id, 'reading_time_words', $count);
}, 10, 3);

الخيار 3 - معالجة نوع منشور مخصص + حقل ACF/Meta مخصص

يمكنك تعديل الطلب إلى post_type => 'portfolio' أو نوع منشور مخصص (CPT) من نوع Avada/Divi. يبقى المبدأ كما هو: دفعة + مؤشر + بيانات وصفية.

التوافق مع Divi 5 / Elementor / Avada

الكود أعلاه غير مرتبط بأي أداة بناء، وهذا مقصود. تتغير أدوات البناء، وتتطور واجهات برمجة التطبيقات الداخلية، لكن REST/Cron يظل مستقرًا.

ديفي 5

  • إذا كنت تريد زر "إعادة حساب وقت القراءة" في لوحة التحكم، فأنشئ صفحة أداة (قائمة) تستدعي نقطة نهاية REST باستخدام JavaScript. X-WP-Nonce.
  • يُمكن لـ Divi 5 عرض الوسم الوصفي (meta tag) عبر وحدة نمطية ديناميكية (بحسب إعداداتك). والأمر الأساسي هو أن الوسم الوصفي موجود مسبقًا، لذا لا حاجة لأي حسابات أثناء العرض.

Elementor

  • يتعامل Elementor مع الحقول الوصفية (الوسوم الديناميكية) بشكل جيد للغاية. بمجرد reading_time بمجرد تخزينها، يمكنك عرضها بدون كتابة أي كود.
  • إذا كان لديك عنصر واجهة مستخدم مخصص يقوم بحساب وقت القراءة على الشاشة، فقم بنقله إلى صفحة القراءة. get_post_meta(get_the_ID(), 'reading_time', true) (مع خيار احتياطي).

أفادا (منشئ الاندماج)

  • يحتوي Avada على عناصر ديناميكية خاصة به. نفس المنطق: الهدف هو نقل الحساب إلى جانب الصيانة، وليس إلى واجهة المستخدم.
  • لتفعيل الصيانة، تجنب استخدام رمز مختصر عام. بدلاً من ذلك، استخدم أداة إدارة أو واجهة سطر أوامر ووردبريس (WP-CLI).

فحوصات ما بعد التثبيت

  • تأكد من تحميل إضافة mu-plugin: في لوحة التحكم، انتقل إلى "الإضافات" ← "الإضافات الإلزامية". ستجد الإضافة الخاصة بك.
  • اختبار REST (بعد تسجيل دخول المسؤول): استخدم عميل REST (أو أداة فحص الشبكة) وقم بالاتصال /wp-json/bpcab/v1/maintenance/reading-time في قسم ما بعد النشر.
  • تأكد من أن مهمة cron قيد التشغيل:
    • باستخدام WP-CLI: wp cron event list | grep bpcab_mk_cron_reading_time
    • ثم wp cron event run --due-now
  • تحقق من النتيجة في منشور: meta reading_time et reading_time_words موجود (عبر إضافة تصحيح الأخطاء أو WP-CLI): wp post meta get 123 reading_time).

مخطط التشخيص السريع

عرض السبب المحتمل التحقق الحلول
تُرجع نقطة النهاية رمز 401 توقيع HMAC مفقود/غير صالح تحقق من العناوين X-BPCAB-* والرسالة الموقعة أعد حساب التوقيع، وتحقق من السرية والمسار الموقّع بدقة.
تُرجع نقطة النهاية رمز 403 قام المستخدم بتسجيل الدخول بدون صلاحية اختبر مع مسؤول النظام استخدم حساب مسؤول أو عدّل الإمكانية (مع توخي الحذر).
"مجدول" ولكن لا شيء يحدث تم تعطيل WP-Cron أو عدم كفاية حركة المرور DISABLE_WP_CRON في ملف wp-config، تحقق من قائمة الأحداث قم بتشغيله عبر WP-CLI أو قم بتكوين مهمة cron للنظام
مهلة / 504 تتم المعالجة عبر بروتوكول HTTP بدلاً من cron سجلات الخادم، وقت استجابة REST احتفظ بالمنطق في cron (الدفعة)، وقلل حجم الدفعة
لم يتم تحديث Meta تم تفعيل التشغيل التجريبي تحقق من الحمولة أعد التشغيل مع dry_run=false

إذا لم ينجح الأمر

  1. تأكد من موقع الملف يجب أن يكون في wp-content/mu-plugins/ليس في plugins/هذا هو الخطأ الأول عند النسخ واللصق.
  2. قم بتفعيل WP_DEBUG في وضع الاختبار وانظر wp-content/debug.logيؤدي غياب قوس أو نسيان فاصلة منقوطة إلى تعطيل الموقع بأكمله (ويحدث ذلك بسرعة عند نسخ جزء من الكود).
  3. اختبار WP-Cron إذا كان موقعك ذو حركة مرور منخفضة، فلن يتم تشغيل WP-Cron تلقائيًا. تشغيل wp cron event run --due-now.
  4. قم بتعطيل ذاكرة التخزين المؤقت مؤقتًا (إضافة + ذاكرة التخزين المؤقت للخادم) إذا كنت تختبر عبر صفحة إدارة/جافا سكريبت. لقد رأيتُ بالفعل نقطة نهاية تظهر على أنها "محظورة" بينما كانت مجرد استجابة مخزنة مؤقتًا على جانب الخادم الوكيل العكسي.
  5. تحقق من PHP إذا كنت تستخدم PHP 7.4/8.0، فقد لا يعمل هذا الكود (بسبب أنواع البيانات الصارمة والتوقيعات). قم بالترقية إلى PHP 8.1 أو أحدث.
  6. النزاعات : إذا قام أحد المكونات الإضافية بالتعديل على مستوى النظام posts_where أو الاستعلامات، اختبرها عن طريق تعطيل إضافات التصفية (تحسين محركات البحث المتقدم، البحث، إلخ).

الأخطاء الشائعة والمزالق

خطأ سبب الحلول
انسخ الكود إلى functions.php من الموضوع الرئيسي تحديث القالب = تم استبدال الكود استخدم إضافة mu أو إضافة مخصصة
Fatal error: Cannot redeclare ... تم تحميل ووردبريس (أو فئة) مرتين لا تقم بتضمين wp-load.php من سياق ووردبريس مُحمّل مسبقًا
الوظيفة لا تنتهي أبداً تم تعطيل WP-Cron (DISABLE_WP_CRON) قم بإعداد مهمة مجدولة للنظام أو نفّذ الأحداث عبر واجهة سطر الأوامر لـ WP-CLI
rest_forbidden / 403 عدم كفاية الصلاحيات أو المستخدم غير الإداري اختبر ذلك مع مسؤول النظام، أو عدّل خاصية رد الاتصال الخاصة بالإذن
401 Signature invalide تم توقيع الرسالة بشكل مختلف (المسار، النص، الطابع الزمني) وقّع بالضبط الطريقة | المسار | الطابع الزمني | نص الرسالة الخام
نتائج غير متسقة تخزن الرموز المختصرة/المنشئات محتوى "غير نصي" اضبط عملية التنظيف (استثنِ بعض الرموز المختصرة، أو احسبها بطريقة مختلفة)
ملف CSS/JS "admin" لا يتم تحميله. خطاف الإضافة إلى قائمة الانتظار غير صحيح (أو لا توجد إضافة إلى قائمة الانتظار) استطلاع رأي حول admin_enqueue_scripts وتحقق من التبعيات
الاختبار مباشرة في بيئة الإنتاج لا يوجد تجهيز مسبق، ولا يوجد نسخ احتياطي مرحلة الإعداد + لقطة سريعة + تجربة أولية
كود من برنامج تعليمي قديم وغير متوافق واجهة برمجة التطبيقات القديمة / أفضل الممارسات راجع الوثائق الرسمية لـ WP 6.9+ وقم بتكييفها (REST/Cron/CLI)

نصائح السلامة والأداء والصيانة

  • لا تجعل نقطة النهاية عامة بدون مصادقة. تُعد نقطة النهاية التي تُفعّل عملية كتابة جماعية إلى قاعدة البيانات هدفًا مثاليًا لهجوم حجب الخدمة (DoS) على التطبيقات.
  • قلل عدد الدفعات وفرض حدود (كما هو الحال في القانون). وبدون ذلك، سيضع أحدهم batch_size=5000 "للانطلاق بسرعة أكبر" وسوف تتلف ذاكرة الوصول العشوائي (RAM) الخاصة بك.
  • قم بتسجيل الدخول إلى الخادم إذا كنت بصدد التوسع الصناعي: يمكنك إضافة error_log() (في مرحلة الاختبار) أو مسجل PSR-3 عبر مكون إضافي، ولكن تجنب تسجيل الأسرار.
  • يفضل استخدام WP-CLI بالنسبة للعمليات الثقيلة، يعتبر REST + Cron جيدًا للتشغيل، لكن WP-CLI أفضل للتنفيذ.
  • مخبأ بعد إعادة حساب بيانات الواجهة الأمامية، قم بمسح ذاكرة التخزين المؤقت للصفحة (الملحق/شبكة توصيل المحتوى) إذا لزم الأمر. يعتقد الكثيرون خطأً أن العملية لم تنجح، بينما هي في الواقع مجرد ذاكرة تخزين مؤقت لملفات HTML.
  • تحسين محركات البحث (SEO) محل reading_time في البيانات الوصفية يكون محايدًا لمحركات البحث، ولكنه يتجنب الحسابات أثناء العرض ويعمل على استقرار وقت استجابة الخادم (TTFB).

الموارد

الأسئلة الشائعة

ماذا يعني بالضبط "استخدام ووردبريس داخل ووردبريس"؟

عمليًا، يعني هذا تنفيذ واجهات برمجة تطبيقات ووردبريس (الطلبات، عمليات CRUD، الخطافات، REST، مهام cron) من سياق غير معتاد: نص برمجي خارجي، مهمة مجدولة، زر أداة، تكامل. يكمن الخطأ في الاعتقاد بأن "تحميل ووردبريس" هو الحل. غالبًا ما يكون ووردبريس مُحملاً بالفعل، وكل ما عليك فعله هو اختيار الخطاف المناسب.

لماذا لا يتم تضمينها ببساطة؟ wp-load.php ?

لأنه هشّ، وغالبًا ما يكون خطيرًا إذا تم كشف الملف. والأهم من ذلك، أنه غير ضروري في 80% من الحالات (أنت تستخدم ووردبريس بالفعل). عندما تحتاج إليه حقًا، استخدم واجهة سطر الأوامر، وليس عبر نقطة نهاية عامة.

هل WP-Cron موثوق؟

يعتمد WP-Cron على حجم الزيارات. قد يُعاني من بعض التأخير في المواقع ذات الزيارات المنخفضة. بالنسبة للمهام الحرجة، استخدم مهمة cron نظامية تستدعي wp-cron.php أو تنفيذ الأحداث عبر WP-CLI.

لماذا يتم تخزين النتيجة في بيانات المنشور بدلاً من حسابها عند العرض؟

لأن واجهة المستخدم الأمامية يجب أن تظل سريعة. حساب التكلفة عند الطلب يضاعف التكلفة بعدد مرات مشاهدة الصفحة. أما التخزين في علامات التعريف (meta tags) فيدفع التكلفة مرة واحدة (أو مع كل تحديث)، وهو الحل الأمثل عمومًا.

هل من المحتمل أن يتعطل هذا الكود مع Divi/Elementor/Avada؟

لا، إنه مستقل. الشيء الوحيد الذي يجب الانتباه إليه هو المحتوى: بعض أدوات الإنشاء تخزن هياكل (JSON/رموز مختصرة) تجعل عدّ الكلمات غير دقيق. إذا كان ناتجك يعتمد بشكل كبير على أدوات الإنشاء، فقم بتعديل منطق التنظيف وفقًا لذلك.

كيفية تشغيل نقطة نهاية REST من لوحة التحكم باستخدام رمز عشوائي (nonce)؟

من لوحة تحكم جافا سكريبت، يمكنك استخدام wpApiSettings.nonce (أو ترجمة نصية) وتقوم بإرسال الترويسة X-WP-Nonceهذا هو تدفق واجهة برمجة تطبيقات REST القياسي على جانب WordPress.

هل يمكن استبدال رمز HMAC بكلمة مرور التطبيق؟

نعم، بالنسبة للاستخدام بين الخوادم، كلمات مرور التطبيق يمكن أن يكون الأمر أبسط. يتميز HMAC بتجنب المصادقة الأساسية وتقليل انكشاف السر "القابل لإعادة الاستخدام". إذا اخترت كلمات مرور التطبيقات، فقم بتقييد المستخدم وصلاحياته.

لماذا يستخدم الكود posts_where بدلاً من مُعامل post__in ?

لأننا نريد مؤشرًا "ID > X" دون تحميل قائمة المعرفات مسبقًا. إزاحة أو post__in قد يصبح الأمر مكلفاً مع الكميات الكبيرة.

ماذا تفعل إذا علقت المهمة في حلقة مفرغة؟

يحدث هذا إذا لم يتحرك مؤشر الماوس (استعلام مُصفّى، أو إضافة تُغيّر الترتيب، أو خلل برمجي). تحقق من الترتيب (orderby ID ASC)، قم بتعطيل المكونات الإضافية التي تقوم بتصفية الطلبات مؤقتًا، وسجل آخر معرف تمت معالجته.

أين يمكنني العثور على سر HMAC؟

بعد تسجيل الدخول كمسؤول (أو عبر WP-CLI)، الخيار bpcab_mk_secret يتم إنشاؤه. يمكنك الحصول عليه باستخدام wp option get bpcab_mk_secretلا تعرضه علنًا أبدًا.