Magento Cacheمن الصفر.
أهم موضوع في أداء Magento. هنفهم طبقات الكاش وترتيبها، أنواع الكاش الداخلية، الـ Full Page Cache و Varnish، الـ tags والإبطال، وإزاي تتحكم في الكاش من كودك وتعمل نوع مخصّص.
الكاش في Magento 2 طبقات: كاش المتصفح، وبعدين الـ Full Page Cache أو Varnish اللي بيرد قبل ما PHP يشتغل، وبعدين الكاش الداخلي زي config و layout و block_html، وأخيرًا قاعدة البيانات والـ indexers. كل صفحة بتتخزّن بـ tags زي cat_p_42 فلما المنتج يتغيّر بيتمسح اللي فيه بس، والمحتوى الشخصي بيتحمّل بالـ private content.
يعني إيه cache أصلاً
الأساس — لو ده واضح، الباقي كله منطقي.
الـ cache هو تخزين نتيجة عملية تقيلة عشان المرة الجاية ترجّعها من غير ما تعيدها. بس كده — مفيش سحر في الموضوع.
- يجي طلب لحاجة (صفحة، قيمة إعداد، نتيجة حساب).
- النظام بيشوف الحاجة دي محفوظة في الكاش ولا لأ.
- لو موجودة (cache hit) — بيرجّعها فوراً وخلاص.
- لو مش موجودة (cache miss) — بيحسبها، يخزّنها، ويرجّعها.
- المرة الجاية بتبقى hit.
التخزين سهل. الصعب هو معرفة امتى النتيجة المخزّنة بقت قديمة ولازم تتلغي. ده اسمه cache invalidation (إبطال الكاش)، وهو مصدر معظم المشاكل — عرض سعر قديم، أو منتج اتشال ولسه ظاهر.
طبقات الكاش في Magento
فيه أكتر من طبقة، وكل واحدة بتشتغل في مرحلة مختلفة.
كل ما الطلب يتوقف عند طبقة أعلى، كل ما كان أسرع. لو Varnish رد على الطلب، Magento مااشتغلش أصلاً — ولا PHP ولا قاعدة بيانات. ده الفرق بين 5 مللي ثانية و 800.
أنواع الكاش الداخلية
اللي بتشوفها في لوحة الأدمن — كل واحد بيخزّن إيه.
الـ Backends — فين بيتخزّن
الكاش لازم يتخزّن في مكان — والاختيار بيفرق كتير.
File (الافتراضي)
ملفات في var/cache. شغّال بدون إعداد، بس بطيء ومش بيتشارك بين سيرفرات.
Redis
في الذاكرة، سريع جداً، ومشترك بين كل السيرفرات. ده اللي تستخدمه في production.
لو الكاش ملفات على كل سيرفر، كل واحد عنده نسخته. تمسح الكاش على سيرفر، والتاني لسه بيقدّم القديم. والـ sessions نفس المشكلة — المستخدم بيتنقل بين السيرفرات فيفقد سلته.
'cache' => [ 'frontend' => [ // الكاش العام (config, layout, block_html...) 'default' => [ 'backend' => 'Cm_Cache_Backend_Redis', 'backend_options' => [ 'server' => '127.0.0.1', 'port' => '6379', // رقم قاعدة بيانات مستقل 'database' => '0', ], ], // كاش الصفحات — قاعدة بيانات منفصلة 'page_cache' => [ 'backend' => 'Cm_Cache_Backend_Redis', 'backend_options' => [ 'server' => '127.0.0.1', 'port' => '6379', 'database' => '1', ], ], ], ],
- default — الكاش العام: الإعدادات، الـ layouts، الـ blocks.
- page_cache — كاش الصفحات الكاملة، منفصل عشان مسحه ما يمسحش الباقي.
- database — رقم قاعدة البيانات في Redis. لازم يكون مختلف لكل غرض.
الـ Full Page Cache
أكبر مكسب أداء — وأكتر واحد بيتعطّل بالغلط.
بدل ما تخزّن أجزاء، تخزّن الصفحة كاملة كـ HTML. أول زائر بيشوف الصفحة بيحسّبها، وكل اللي بعده بياخدوا النسخة الجاهزة.
من غير FPC
كل طلب: PHP يشتغل، مئات الاستعلامات، بناء الصفحة. مئات المللي ثانية.
مع FPC
الصفحة الجاهزة بترجع مباشرة. ميلي ثواني معدودة.
الصفحات العامة اللي كل الزوار بيشوفوها زي بعض: الرئيسية، الأقسام، المنتجات، صفحات CMS.
الصفحات الشخصية: السلة، الـ checkout، حساب العميل. لأن محتواها بيختلف من زائر للتاني — ومينفعش تعرض سلة واحد لواحد تاني.
Varnish
الطبقة اللي قدّام كل حاجة.
- الطلب بيوصل Varnish الأول (مش Magento).
- Varnish بيشوف الصفحة عنده ولا لأ.
- لو عنده — بيرد فوراً وMagento مااشتغلش خالص.
- لو مش عنده — بيمرّر الطلب لـ Magento.
- Magento بيبني الصفحة ويرجّعها مع headers بتقول "دي تتكاش وde مدتها".
- Varnish بيخزّنها ويرجّعها للزائر. الطلب الجاي هيتوقف عند خطوة 3.
من الأدمن: Stores → Configuration → Advanced → System → Full Page Cache، اختار Varnish Caching. وبعدها تصدّر ملف الإعداد (VCL) وتحطه على سيرفر Varnish.
# بيولّد ملف VCL مناسب لنسختك
bin/magento varnish:vcl:generate \
--export-version=6 \
--backend-host=localhost \
--backend-port=8080 \
--output-file=/etc/varnish/default.vcl
curl -I https://your-store.com/some-category # دوّر على السطور دي في الرد: # X-Magento-Cache-Debug: HIT ← الصفحة من الكاش ✓ # X-Magento-Cache-Debug: MISS ← اتبنت دلوقتي # Age: 320 ← قاعدة في الكاش 320 ثانية
المحتوى الخاص بالزائر
المعضلة: الصفحة متكاشة، فإزاي أعرض "أهلاً أحمد"؟
الصفحة الرئيسية متكاشة ونسخة واحدة للكل. بس فيها اسم العميل وعدد منتجات السلة — وديهما مختلفين لكل زائر. لو كاشتهم، كل واحد هيشوف بيانات غيره.
Magento بيفصل الصفحة لجزئين: الهيكل العام (متكاش ومشترك) والأجزاء الشخصية (بتتحمّل بعدين بـ JavaScript من الـ customer-data section).
define(['Magento_Customer/js/customer-data'], function (customerData) { 'use strict'; // بيرجّع كائن قابل للمراقبة var cart = customerData.get('cart'); var customer = customerData.get('customer'); // بيتحدّث تلقائياً لما البيانات تتغيّر console.log(cart().summary_count); console.log(customer().firstname); });
<?xml version="1.0"?> <config> <!-- لما الـ action دي تتنفّذ، حدّث القسم --> <action name="vendor/loyalty/redeem"> <section name="loyalty-points"/> </action> </config>
namespace Vendor\Module\CustomerData; use Magento\Customer\CustomerData\SectionSourceInterface; class LoyaltyPoints implements SectionSourceInterface { public function __construct( private readonly Session $customerSession, private readonly PointsRepository $points ) {} // بيرجّع البيانات اللي هتوصل للـ JS public function getSectionData(): array { if (!$this->customerSession->isLoggedIn()) { return ['points' => 0]; } $customerId = $this->customerSession->getCustomerId(); return [ 'points' => $this->points->getBalance($customerId), ]; } }
<type name="Magento\Customer\CustomerData\SectionPoolInterface"> <arguments> <argument name="sectionSourceMap" xsi:type="array"> <item name="loyalty-points" xsi:type="string"> Vendor\Module\CustomerData\LoyaltyPoints </item> </argument> </arguments> </type>
الـ Tags والإبطال
الجزء الذكي — إزاي Magento بيعرف يمسح إيه بالظبط.
منتج واحد اتغيّر سعره. الصفحات اللي فيها المنتج ده لازم تتمسح — بس مش كل الصفحات. لو مسحت كله، المتجر هيبطّئ بدون داعي.
كل صفحة متكاشة بتتخزّن مع لافتات (tags) بتقول "الصفحة دي فيها المنتجات كذا وكذا والأقسام كذا". لما منتج يتغيّر، Magento بيمسح الصفحات اللي عليها اللافتة دي بس.
# صفحة منتج رقم 42 ممكن يكون عليها: cat_p_42 # المنتج نفسه cat_c_15 # القسم اللي فيه cat_p # كل المنتجات (عام) # لما المنتج 42 يتغيّر، Magento بيمسح # كل حاجة عليها cat_p_42 — ودي الصفحات # اللي المنتج ده ظاهر فيها بس
class FeaturedProducts extends Template implements IdentityInterface { // Magento بينده دي عشان يعرف الصفحة دي // تتمسح امتى public function getIdentities(): array { $identities = []; foreach ($this->getProducts() as $product) { // كل منتج بيضيف لافتته $identities = array_merge( $identities, $product->getIdentities() ); } return $identities; } }
التحكم في كاش الـ Block
إزاي تخلّي block يتكاش أو لأ.
<!-- block عادي — بيتكاش --> <block class="Vendor\Module\Block\Info" name="my.info" template="Vendor_Module::info.phtml"/> <!-- block مش بيتكاش --> <block class="Vendor\Module\Block\Live" name="my.live" cacheable="false"/>
لو محتاج محتوى متغيّر في صفحة متكاشة، استخدم الـ private content (القسم 07) — الصفحة تفضل متكاشة والجزء المتغيّر بيتحمّل بالـ JavaScript.
class MyBlock extends Template { protected function _construct() { // مدة الكاش بالثواني (ساعة) $this->addData([ 'cache_lifetime' => 3600, ]); } // المفاتيح اللي بتميّز نسخ الـ block public function getCacheKeyInfo(): array { return [ 'MY_BLOCK', $this->_storeManager->getStore()->getId(), $this->getData('category_id'), ]; } }
الـ getCacheKeyInfo() بيحدد إيه اللي بيخلّي نسخة تختلف عن نسخة. في المثال ده، الـ block هيتخزّن بنسخة مختلفة لكل متجر ولكل قسم. لو نسيت تضيف متغيّر مؤثّر، هتعرض نسخة غلط.
استخدام الكاش في كودك
لما تحتاج تخزّن نتيجة عملية تقيلة بنفسك.
namespace Vendor\Module\Model; use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; class ExpensiveData { // مفتاح مميّز للبيانات دي private const CACHE_KEY = 'vendor_expensive_data'; // لافتة عشان نقدر نمسحها انتقائياً private const CACHE_TAG = 'vendor_expensive'; // المدة بالثواني — ساعة private const LIFETIME = 3600; public function __construct( private readonly CacheInterface $cache, private readonly SerializerInterface $serializer, private readonly ApiClient $api ) {} public function getData(): array { // 1. جرّب الكاش الأول $cached = $this->cache->load(self::CACHE_KEY); if ($cached) { // لقيناها — رجّعها بعد فك التحويل return $this->serializer->unserialize($cached); } // 2. مش موجودة — احسبها $data = $this->api->fetchSomethingSlow(); // 3. خزّنها للمرة الجاية $this->cache->save( $this->serializer->serialize($data), self::CACHE_KEY, [self::CACHE_TAG], // اللافتات self::LIFETIME // المدة ); return $data; } // لمسح الكاش ده تحديداً public function invalidate(): void { $this->cache->clean([self::CACHE_TAG]); } }
- load() — بيجيب القيمة أو false لو مش موجودة.
- serializer — الكاش بيخزّن نصوص بس، فلازم تحوّل المصفوفات. متستخدمش serialize() العادية — الـ interface ده أأمن.
- الـ tags — بتخليك تمسح البيانات دي بس بدل الكاش كله.
- الـ lifetime — بالثواني. حط null لمدة غير محدودة (بحذر).
عمل نوع كاش مخصّص
عشان يظهر في لوحة الأدمن مع باقي الأنواع.
عشان فريق التشغيل يقدر يمسحه من الأدمن من غير ما يحتاج سطر أوامر، وعشان يتفصل عن باقي الكاش فمسح نوع ما يأثرش على التاني.
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <type name="vendor_catalog_feed" translate="label,description" instance="Vendor\Module\Model\Cache\Type\Feed"> <label>Catalog Feed</label> <description>Cached product feed data</description> </type> </config>
namespace Vendor\Module\Model\Cache\Type; use Magento\Framework\Cache\Frontend\Decorator\TagScope; use Magento\Framework\App\Cache\Type\FrontendPool; class Feed extends TagScope { // لازم يطابق الـ name في cache.xml public const TYPE_IDENTIFIER = 'vendor_catalog_feed'; // اللافتة اللي كل إدخالات النوع ده هتاخدها public const CACHE_TAG = 'VENDOR_CATALOG_FEED'; public function __construct(FrontendPool $cacheFrontendPool) { parent::__construct( $cacheFrontendPool->get(self::TYPE_IDENTIFIER), self::CACHE_TAG ); } }
use Vendor\Module\Model\Cache\Type\Feed; class FeedGenerator { public function __construct( // بتحقن النوع بتاعك مباشرة private readonly Feed $cache ) {} public function get(string $key): ?string { $value = $this->cache->load($key); return $value !== false ? $value : null; } public function set(string $key, string $data): void { // اللافتة بتتضاف تلقائياً $this->cache->save($data, $key, [], 3600); } }
bin/magento setup:upgrade bin/magento cache:enable vendor_catalog_feed # دلوقتي بيظهر في قائمة الأنواع bin/magento cache:status # ومسحه لوحده ممكن bin/magento cache:clean vendor_catalog_feed
الأوامر والتشخيص
اللي هتستخدمه كل يوم.
# شوف حالة كل الأنواع bin/magento cache:status # امسح الإدخالات القديمة (الأخف) bin/magento cache:clean # امسح نوع محدد — الأفضل في production bin/magento cache:clean config layout block_html # امسح كل حاجة بما فيها إدخالات تطبيقات تانية bin/magento cache:flush # عطّل نوع (للتطوير) bin/magento cache:disable block_html # فعّل تاني bin/magento cache:enable block_html
clean
بيمسح إدخالات Magento المعلّمة بلافتاته بس. أخف وأأمن.
flush
بيفضّي مخزن الكاش كله — حتى بيانات تطبيقات تانية بتشارك نفس Redis.
# هل الصفحة بتتكاش؟ curl -I https://store.com/page | grep -i "x-magento-cache" # حجم الكاش في Redis redis-cli -n 0 INFO keyspace redis-cli -n 1 INFO keyspace # دوّر على blocks معطّلة للكاش (السبب الشائع) grep -rn 'cacheable="false"' app/code/ vendor/ \ --include="*.xml"
# عطّل الأنواع اللي بتعطّلك أثناء التطوير bin/magento cache:disable block_html full_page layout # سيبهم مفعّلين — مسحهم مكلّف وبطيء # config, db_ddl, compiled_config
الفخاخ الشائعة
اقرا دي حتى لو مش هتقرا حاجة تانية.
- cacheable="false" على block صغير — بيلغي كاش الصفحة كلها. أخطر فخ على الإطلاق.
- block بيعرض منتجات من غير IdentityInterface — أسعار قديمة بتفضل ظاهرة، بصمت.
- نسيان متغيّر في getCacheKeyInfo — عميل بيشوف نسخة عميل تاني.
- نفس رقم Redis database لبيئتين — البيئات بتدهس كاش بعض، والأعراض عشوائية.
- flush بدل clean على production — بيمسح كاش مواقع تانية بتشارك Redis.
- مسح الكاش كله وقت الذروة — كل الطلبات تضرب PHP مرة واحدة والسيرفر ممكن يقع.
- if ($cached) مع قيم ممكن تكون صفر — الكاش شغّال ومالوش فايدة.
- بيانات حسّاسة في private content sections — بتتخزّن في متصفح الزائر.
- قياس الأداء والكاش معطّل — نتيجة بلا معنى.
- الاعتماد على مدة انتهاء طويلة بدل الـ tags — بيانات قديمة لساعات بدل ما تتمسح فوراً.
أسئلة الإنترفيو
من أكتر المواضيع اللي بتتسأل فيها.
Magento Cache ✓
دلوقتي فاهم الكاش من الصفر: الطبقات وترتيبها، الأنواع الداخلية، الـ FPC و Varnish، المحتوى الشخصي، الـ tags والإبطال، والتحكم من كودك.
الخلاصة: كل ما الطلب وقف عند طبقة أعلى كان أسرع، والـ tags هي اللي بتخلّي المسح انتقائي بدل شامل، وcacheable="false" بيلغي الصفحة كلها مش الـ block.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إيه الفرق بين Varnish والـ Built-in Full Page Cache في Magento 2؟
الـ Built-in بيخزّن الصفحات جوّه Magento في Redis أو ملفات، فلازم PHP يشتغل ويحمّل Magento عشان يرد حتى لو الصفحة موجودة. Varnish سيرفر مستقل قدّام Magento بيرد قبل ما PHP يتنده أصلًا، فهو أسرع بمراحل. الـ Built-in للتطوير والمشاريع الصغيرة، و Varnish هو المعيار في production ومتاح من Stores › Configuration › Advanced › System › Full Page Cache.
ليه cacheable="false" بيبطّئ Magento؟
لأنه مابيلغيش كاش الـ block ده بس، بيلغي كاش الصفحة كلها. block صغير في الـ footer معلّم cacheable="false" بيخلّي كل صفحات الموقع MISS في الـ Full Page Cache. لو الصفحات كلها MISS، ابدأ بـ grep -rn 'cacheable="false"' app/code/ vendor/، وغالبًا هتلاقيه في موديول خارجي. البديل الصح للمحتوى المتغيّر هو الـ private content.
إزاي Magento 2 بيعرف يمسح أنهي صفحات لما منتج يتغيّر؟
بنظام الـ cache tags. كل صفحة بتتخزّن مع لافتات بتقول فيها إيه، زي cat_p_42 للمنتج و cat_c_15 للقسم. لما المنتج يتحفظ، Magento بيمسح كل اللي عليه اللافتة دي بس. الـ block بتاعك لازم يطبّق IdentityInterface ويرجّع identities المنتجات اللي بيعرضها من getIdentities()، وإلا الصفحة هتفضل تعرض أسعار قديمة.
إزاي أعرض اسم العميل أو السلة في صفحة متكاشة؟
بالـ private content. الصفحة بتفضل متكاشة ونسخة واحدة للكل، والأجزاء الشخصية بتتحمّل بـ JavaScript من الـ customer-data sections. بتعمل كلاس بيطبّق SectionSourceInterface وتسجّله في etc/frontend/di.xml في sectionSourceMap، وتربط الـ actions اللي بتغيّره في etc/frontend/sections.xml. البيانات دي بتتخزّن في localStorage، فمتحطش فيها حاجة حسّاسة.
إيه الفرق بين cache:clean و cache:flush في Magento 2؟
cache:clean بيمسح إدخالات Magento المعلّمة بالـ tags بتاعته بس، وتقدر تحدّد أنواع معيّنة زي config و layout. cache:flush بيفضّي مخزن الكاش كله، حتى بيانات تطبيقات تانية بتشارك نفس Redis. الافتراضي في production يكون clean لأنواع محدّدة، لأن مسح كل حاجة وقت الذروة بيخلّي كل الطلبات تضرب PHP مرة واحدة.
إزاي أخزّن نتيجة عملية تقيلة في كاش Magento 2 من كودي؟
احقن Magento\Framework\App\CacheInterface و SerializerInterface. جرّب load($key) الأول، ولو رجّع false احسب البيانات واحفظها بـ save($serialized, $key, [$tag], $lifetime). الـ tag بيخلّيك تمسح البيانات دي بس بـ clean([$tag]). خلّي بالك إن load بيرجّع false لما مفيش قيمة، فقارن بـ !== false لو القيمة ممكن تكون صفر أو نص فاضي.
إزاي أعمل نوع كاش مخصّص يظهر في Cache Management؟
عرّفه في etc/cache.xml بـ name و label و description و instance، واعمل كلاس بيورّث من Magento\Framework\Cache\Frontend\Decorator\TagScope فيه TYPE_IDENTIFIER مطابق للاسم و CACHE_TAG، وبيمرّر $cacheFrontendPool->get(self::TYPE_IDENTIFIER) للـ parent. بعد setup:upgrade و cache:enable vendor_catalog_feed بيظهر في الأدمن وفريق التشغيل يمسحه لوحده من غير سطر أوامر.