ترقية Pimcore من 10 لـ 12: مسار الترحيل الحقيقي
الدليل الشامل لترقية Pimcore 10 لـ 12. PHP 8.3، Symfony 7، DBAL 4، ترحيل collation، نظام الأحداث، ExtJS 7، Redis، OpenSearch، Kubernetes، Flysystem، وكل الأخطاء اللي ما لقيناها بالتوثيق.
ليش ما تقدر تتخطى Pimcore 11
أول سؤال كل فريق يسأله: نقدر نقفز مباشرة من 10 لـ 12؟ لا. Pimcore 11 هو المحطة الإجبارية. التغييرات بالإطار كبيرة جداً تتشرّب بخطوة وحدة، وأدوات الترحيل من Pimcore نفسها تفترض إنه الإصدار الوسيط موجود.
هاي الصورة الكاملة لشو يتغير عبر الإصدارين الرئيسيين:
| المكوّن | Pimcore 10.6 | Pimcore 11 | Pimcore 12 |
|---|---|---|---|
| PHP | 8.0+ | 8.1+ | 8.3+ |
| Symfony | 5.4 | 6.2+ | 6.4 / 7.x |
| Doctrine DBAL | 2.x / 3.x | 3.x | 4.x |
| واجهة الإدارة | مدمجة (ExtJS 6) | حزمة منفصلة (ExtJS 6/7) | حزمة منفصلة (ExtJS 7) + Studio UI (React) |
| الرخصة | GPLv3 | GPLv3 | POCL (Pimcore Open Core License) |
| الحزم الأساسية | متراصة | مستخرجة | مستخرجة |
| محرك البحث | مدمج | Simple Backend Search Bundle | Generic Data Index + OpenSearch |
| محرر WYSIWYG | TinyMCE مدمج | حزمة TinyMCE | حزمة TinyMCE أو Quill |
| تسجيل المنتج | مش مطلوب | مش مطلوب | مطلوب (تحقق offline) |
| نظام الأحداث (JS) | Plugin Broker | Event Listeners | Event Listeners + أحداث Studio UI |
| Collation | utf8mb4_general_ci | utf8mb4_general_ci | utf8mb4_unicode_520_ci (إجباري) |
| الكاش | محول Pimcore | محول Symfony | محول Symfony (تغيير بالإعدادات) |
| كاش الصفحة الكاملة | مدمج | مدمج | خيار حزمة منفصلة |
| تخزين الأصول | محلي / Flysystem | محلي / Flysystem | محلي / Flysystem (مع أخطاء على التخزين البعيد) |
سوّينا هالترقية على عدة مشاريع Pimcore مؤسسية تتراوح من أنظمة PIM لشركات B2B لمنصات CMS متعددة المواقع. الأنماط الموصوفة هنا عامة وما ترتبط بمنتج محدد. للسياق عن كيف نتعامل مع تنفيذات PIM وبنية الأنظمة، هالأدلة تغطي منهجيتنا الأوسع. صفحة المنهجية توضح كيف نخطط لهالنوع من الترحيلات عالية المخاطر.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Pimcore 10.6 │ ──▶ │ Pimcore 11.x │ ──▶ │ Pimcore 12.x │
│ Symfony 5.4 │ │ Symfony 6.2+ │ │ Symfony 6.4/7 │
│ PHP 8.0+ │ │ PHP 8.1+ │ │ PHP 8.3+ │
│ DBAL 2.x/3.x │ │ DBAL 3.x │ │ DBAL 4.x │
│ ExtJS 6 │ │ ExtJS 6/7 │ │ ExtJS 7 + │
│ │ │ │ │ Studio UI │
└─────────────────┘ └─────────────────┘ └─────────────────┘
المرحلة 1: التحضير (قبل ما تلمس Composer)
هالمرحلة تاخذ أطول وقت وتوفّر أكثر وقت. كل ساعة تقضيها هنا توفّر أيام من تتبع الأخطاء بعدين.
1.1 قائمة التحقق قبل الترقية
# تحقق من الإصدار الحالي
bin/console --version
# تأكد إنه كل الترحيلات محدّثة
bin/console doctrine:migrations:up-to-date
bin/console doctrine:migrations:migrate
# فضّي كل طوابير الرسائل قبل الترقية
# العمال لازم يخلصوا المعالجة قبل ما تغير الكود
bin/console messenger:consume --limit=0
خذ نسخة احتياطية من كل شي: قاعدة البيانات (mysqldump كامل)، الملفات (مجلد الأصول، مجلد var، مجلد config)، وأنشئ فرع ترقية مخصص بنظام التحكم بالإصدارات. جرّب إجراء الاستعادة قبل ما تبدأ.
1.2 دوال Doctrine المهملة (DBAL 2/3 لـ 4)
هاي أكثر خطوة تحضيرية تاخذ جهد. كل كلاس repository، كل استعلام SQL خام، كل خدمة مخصصة تلمس قاعدة البيانات تحتاج تحديث. بمشروع مؤسسي نموذجي، توقّع 50 لـ 150 مكان.
| المهمل (DBAL 2/3) | البديل (DBAL 4) | ملاحظات |
|---|---|---|
$db->query($sql) | $db->executeQuery($sql) | نوع الإرجاع صار Result |
$db->executeUpdate($sql) | $db->executeStatement($sql) | يرجع عدد الصفوف المتأثرة كـ int |
$db->fetchRow() | $db->fetchAssociative() | يرجع مصفوفة ترابطية أو false |
$db->fetchAll() | $db->fetchAllAssociative() | يرجع مصفوفة من المصفوفات الترابطية |
$db->fetchCol() | $db->fetchFirstColumn() | يرجع مصفوفة مفهرسة |
$db->fetchColumn() | $db->fetchOne() | جلب قيمة واحدة |
$db->fetchPairs() | Pimcore\Db\Helper::fetchPairs() | مساعد Pimcore |
$db->insertOrUpdate() | Pimcore\Db\Helper::upsert() | مساعد Pimcore |
$db->quoteInto() | Pimcore\Db\Helper::quoteInto() | مساعد Pimcore |
$db->deleteWhere() | $db->executeStatement() مع DELETE | SQL يدوي |
$db->updateWhere() | $db->executeStatement() مع UPDATE | SQL يدوي |
$db->quote($val, $type) | $db->quote($val) | سلاسل نصية فقط بـ DBAL 4، بدون معامل النوع |
Pimcore\Db\ConnectionInterface | Doctrine\DBAL\Connection | تغيير type hint بـ DI |
Pimcore\Db\Connection | Doctrine\DBAL\Connection | استبدال الكلاس |
# ابحث عن كل استدعاءات الدوال المهملة بالكود
grep -rn "->query(" src/ --include="*.php"
grep -rn "->fetchRow(" src/ --include="*.php"
grep -rn "->fetchAll(" src/ --include="*.php" | grep -v "fetchAllAssociative"
grep -rn "->fetchCol(" src/ --include="*.php"
grep -rn "->insertOrUpdate(" src/ --include="*.php"
grep -rn "->quoteInto(" src/ --include="*.php"
grep -rn "->deleteWhere(" src/ --include="*.php"
grep -rn "->executeUpdate(" src/ --include="*.php"
grep -rn "Pimcore\\Db\\Connection" src/ --include="*.php"
مثال ترحيل كامل لكلاس repository:
// قبل (Pimcore 10 / DBAL 2-3)
use Pimcore\Db\ConnectionInterface;
class PriceExporter
{
public function __construct(
private readonly ConnectionInterface $connection
) {}
public function export(int $limit, int $offset): array
{
$sql = "SELECT p.o_id, p.price FROM object_store_1 p WHERE p.o_published = 1 LIMIT ? OFFSET ?";
return $this->connection->query($sql)->fetchAll();
}
}
// بعد (Pimcore 12 / DBAL 4)
use Doctrine\DBAL\Connection;
class PriceExporter
{
public function __construct(
private readonly Connection $connection
) {}
public function export(int $limit, int $offset): array
{
$sql = "SELECT p.id, p.price FROM object_store_1 p WHERE p.published = 1 LIMIT ? OFFSET ?";
return $this->connection->executeQuery($sql, [$limit, $offset])->fetchAllAssociative();
}
}
حدّث ملف services.yaml إذا كنت تستخدم حقن التبعيات الصريح:
services:
App\Export\PriceExporter:
arguments:
$connection: '@doctrine.dbal.default_connection'
1.3 تصريحات أنواع الإرجاع
Pimcore 11+ يفرض أنواع إرجاع صارمة على كلاسات النماذج. كل كلاس يمدد كلاسات Pimcore الأساسية يحتاج تحديث:
// قبل (Pimcore 10)
public function save()
{
// منطق مخصص
parent::save();
}
// بعد (Pimcore 11+)
public function save(array $parameters = []): static
{
// منطق مخصص
return parent::save($parameters);
}
هالشي يأثر على كل كلاس DataObject model، كل listing مخصص، وكل خدمة تعمل override لدوال Pimcore. تحقق من كل الكلاسات بمجلد src/Model/.
1.4 إزالة بادئة o_
Pimcore 11 يزيل بادئة o_ من أسماء الأعمدة بجدول objects. كل استعلام SQL خام وكل شرط listing يشير لهالأعمدة يحتاج تحديث:
| القديم (Pimcore 10) | الجديد (Pimcore 11+) | السياق |
|---|---|---|
o_id | id (أو oo_id بجداول object store) | المفتاح الرئيسي |
o_creationDate | creationDate | الطابع الزمني |
o_modificationDate | modificationDate | الطابع الزمني |
o_path | path | مسار الشجرة |
o_key | key | مفتاح/slug الكائن |
o_published | published | علم النشر |
o_parentId | parentId | مرجع الأب |
o_type | type | نوع الكائن |
o_className | className | اسم الكلاس |
o_classId | classId | معرّف الكلاس |
o_userOwner | userOwner | معرّف المستخدم المنشئ |
o_userModification | userModification | معرّف آخر معدّل |
o_versionCount | versionCount | عداد الإصدارات |
# ابحث عن كل استخدام بادئة o_ بـ PHP و JavaScript
grep -rn "o_id\|o_path\|o_key\|o_published\|o_parentId\|o_type\|o_className\|o_classId" src/ --include="*.php"
grep -rn "o_className\|o_id" public/ --include="*.js"
مثال ترحيل شرط listing:
// قبل
$listing->setCondition('o_path LIKE ? AND o_published = 1', ['/products/%']);
// بعد
$listing->setCondition('path LIKE ? AND published = 1', ['/products/%']);
1.5 تغييرات PHP 8.3 / 8.4 الجذرية
بعيداً عن متطلبات Pimcore، PHP 8.3+ عنده تغييرات جذرية خاصة فيه:
المعاملات القابلة للـ null ضمنياً (مهملة بـ 8.4):
// مهمل: nullable ضمني
public function findByLocale(string $locale = null): array
// مصلّح: nullable صريح
public function findByLocale(?string $locale = null): array
هالشي يأثر على كل توقيع دالة فيه = null كقيمة افتراضية. بمشروع مؤسسي نموذجي، توقّع 50+ حالة. استخدم Rector للأتمتة:
// rector.php
use Rector\Config\RectorConfig;
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src']);
$rectorConfig->rule(ExplicitNullableParamTypeRector::class);
};
# معاينة التغييرات
vendor/bin/rector process --dry-run
# تطبيق التغييرات
vendor/bin/rector process
ميزات PHP 8.3+ ثانية تقدر تتبناها:
// ثوابت كلاس مصنّفة (PHP 8.3)
public const string DEFAULT_LOCALE = 'en';
public const array SUPPORTED_TYPES = ['product', 'category'];
// خاصية #[\Override] (PHP 8.3)
#[\Override]
public function normalize(mixed $object, ?string $format = null, array $context = []): array
// json_validate() (PHP 8.3)
if (json_validate($input)) { /* ... */ }
1.6 ترحيل نظام أحداث JavaScript
نظام Plugin Broker القديم محذوف بـ Pimcore 11. كل كود JavaScript لواجهة الإدارة لازم يتهاجر:
// قبل (Pimcore 10 - Plugin Broker)
pimcore.plugin.datasheet = Class.create(pimcore.plugin.admin, {
getClassName: function () {
return "pimcore.plugin.datasheet";
},
initialize: function () {
pimcore.plugin.broker.registerPlugin(this);
},
postOpenObject: function (object, type) {
if (object.data.general.o_className === 'Product') {
// إضافة زر شريط أدوات
}
}
});
// بعد (Pimcore 11+ - Event Listeners)
(function() {
'use strict';
document.addEventListener(pimcore.events.postOpenObject, (e) => {
let object = e.detail.object;
if (object.data.general.className === 'Product') { // o_className -> className
object.toolbar.add({
text: t('Generate PDF'),
iconCls: 'pimcore_icon_pdf',
handler: function () {
Ext.Ajax.request({
url: '/api/generate-pdf/' + object.id,
success: function (response) {
pimcore.helpers.showNotification(
t("success"), t("PDF generated"), "success"
);
}
});
}
});
pimcore.layout.refresh();
}
});
})();
تغييرات JavaScript إضافية:
| القديم (Pimcore 10) | الجديد (Pimcore 11+) | ملاحظات |
|---|---|---|
ts() | t() | دالة الترجمة |
pimcore.helpers.addCsrfTokenToUrl() | احذفها بالكامل | CSRF يتم التعامل معه بشكل مختلف |
o_className بـ JS | className | يطابق تغيير PHP |
Class.create(...) | لسا مدعوم، لكن يُفضل كلاسات ES6 | تحضير للمستقبل |
/admin/tags/* | ممكن يتغير لـ /pimcore-admin/tags/* | تحقق من مسارات الإدارة |
1.7 تغييرات ExtJS 7 (Pimcore 12)
Pimcore 12 يستخدم ExtJS 7.x. التغييرات الجذرية الرئيسية لكود واجهة الإدارة المخصص:
// ExtJS 6 (Pimcore 10/11)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
root: 'data' // مهمل بـ ExtJS 7
}
}
});
// ExtJS 7 (Pimcore 12)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
rootProperty: 'data' // استخدم rootProperty
}
}
});
تحقق من كل grid operators المخصصة، إضافات الإدارة، وإضافات استيراد البيانات للتوافق مع ExtJS 7. المشاكل الشائعة:
- خاصية
rootتمت إعادة تسميتها لـrootPropertyبـ store readers - بعض إعدادات أعمدة grid تغيرت
- عمل patching على الـ prototype (
Ext.override) ممكن ينكسر إذا تغيرت بنية الكلاس الداخلية - تحقق من كل مراجع
iconClsإنها لسا موجودة بمجموعة أيقونات Pimcore 12
1.8 واجهة Pimcore Studio UI (React)
Pimcore 12 يقدم واجهة Studio UI الجديدة المبنية بـ React، تشتغل جنب واجهة ExtJS الكلاسيكية. هاي مش بديل بعد، إضافة:
- Studio UI تتعامل مع بعض الميزات الجديدة (واجهة Generic Data Index، بعض إدارة الأصول)
- واجهة الإدارة الكلاسيكية (ExtJS) تبقى الواجهة الرئيسية لمعظم العمليات
- إضافات ExtJS المخصصة تستمر بالعمل بواجهة الإدارة الكلاسيكية
- نقاط التوسعة الجديدة لـ Studio UI تستخدم مكونات React
- الاتجاه طويل المدى نحو React، لكن الترحيل الكامل مش مطلوب لـ P12
إذا عندك إضافات واجهة إدارة مخصصة، رح تستمر بالعمل بالواجهة الكلاسيكية. ما في ترحيل فوري لـ React مطلوب. لكن إذا عم تبني وظائف إدارة جديدة، فكّر إنك تبنيها لـ Studio UI بدلاً. كتبنا شرح مفصل عن بناء حزمة Pimcore 12 بمستوى إنتاجي مع تكامل Studio UI يغطي العملية الكاملة من هيكل الحزمة لتسجيل مكونات React.
1.9 إزالة SensioFrameworkExtraBundle
هالحزمة مهملة ومحذوفة بـ Symfony 6. استبدل كل التعليقات التوضيحية:
// قبل (Sensio)
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/** @Template("default/default.html.twig") */
public function defaultAction()
{
return [];
}
// بعد (Symfony 6+)
public function defaultAction(): Response
{
return $this->render('default/default.html.twig', []);
}
1.10 تحويل Symfony Route Annotations لـ Attributes
كل ملفات الـ controllers (15+ ملف) تحتاج هالتغيير:
// قبل (Symfony 5.4 annotations)
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/{_locale}/{slug}_p{id}",
* name="product_detail",
* methods={"GET"},
* requirements={"slug"="[\w\s\-\.]+", "id"="[\w\. ]+"}
* )
*/
public function __invoke(Request $request, string $id): Response
// بعد (Symfony 6.4+ attributes)
use Symfony\Component\Routing\Attribute\Route;
#[Route(
path: '/{_locale}/{slug}_p{id}',
name: 'product_detail',
methods: ['GET'],
requirements: ['slug' => '[\w\s\-\.]+', 'id' => '[\w\. ]+']
)]
public function __invoke(Request $request, string $id): Response
حدّث ملف config/packages/routing.yaml:
# غيّر النوع من 'annotation' لـ 'attribute'
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
1.11 تغييرات Symfony Serializer (Symfony 7.x)
إذا عندك Normalizers مخصصة (شائعة لفهرسة البحث وتسلسل API)، Symfony 7.x يتطلب دالة جديدة:
// قبل (Symfony 5.4/6.x)
class ProductNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, string $format = null, array $context = []): array
{
// ...
}
public function supportsNormalization(mixed $data, string $format = null): bool
{
return $data instanceof Product;
}
}
// بعد (Symfony 7.x)
class ProductNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
// ...
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof Product;
}
// جديد: مطلوب بـ Symfony 7.x
public function getSupportedTypes(?string $format): array
{
return [
Product::class => true,
];
}
}
إذا عندك 10+ normalizers (نموذجي لفهرسة MeiliSearch أو OpenSearch)، هالتغيير متكرر لكن حرج. عدم وجود getSupportedTypes() يسبب خطأ fatal على Symfony 7.x.
1.12 إهمال Request::get()
Request::get() مهملة بـ Symfony 6.x. استخدم دوال الوصول المحددة:
// مهمل
$value = $request->get('param');
// استخدم الدوال المحددة
$value = $request->query->get('param'); // معاملات GET
$value = $request->request->get('param'); // معاملات POST
$value = $request->attributes->get('param'); // معاملات المسار
$value = $request->headers->get('param'); // الترويسات
$value = $request->query->getBoolean('flag'); // معامل GET منطقي
1.13 تثبيت جسر التوافق
composer require --no-update pimcore/compatibility-bridge-v10
composer require --no-update symfony/dotenv
المرحلة 2: الترقية لـ Pimcore 11
2.1 تحديث Composer
{
"require": {
"pimcore/pimcore": "^11.0",
"pimcore/admin-ui-classic-bundle": "^1.0"
}
}
composer remove --no-update sensio/framework-extra-bundle
COMPOSER_MEMORY_LIMIT=-1 composer update -W
توقّع إنه حل التبعيات يواجه صعوبات مع الحزم الخارجية. تواصل مع مزودي الحزم للحصول على إصدارات متوافقة مع P11 أو ابحث عن بدائل.
2.2 تسجيل حزمة الإدارة
// src/Kernel.php
public function registerBundlesToCollection(BundleCollection $collection): void
{
$collection->addBundle(new \Pimcore\Bundle\AdminBundle\PimcoreAdminBundle(), 60);
}
2.3 تثبيت الحزم المستخرجة
كثير من الميزات الأساسية صارت حزم منفصلة. ثبّت بس اللي تستخدمه فعلاً:
composer require \
pimcore/static-routes-bundle \
pimcore/custom-reports-bundle \
pimcore/application-logger-bundle \
pimcore/seo-bundle \
pimcore/simple-backend-search-bundle \
pimcore/glossary-bundle \
pimcore/tinymce-bundle \
pimcore/xliff-bundle \
pimcore/word-export-bundle
سجّل كل واحدة بـ config/bundles.php.
2.4 تحديث إعدادات المسارات
# config/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing.yaml" # كان: Resources/config/routing.yml
# config/routes/dev/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing_dev.yaml" # كان: Resources/config/routing_dev.yml
2.5 إعدادات Messenger Transport
Pimcore 11 يقدم ناقلات رسائل async جديدة:
framework:
messenger:
transports:
# الناقلات الموجودة...
# جديد لـ Pimcore 11+
pimcore_scheduled_tasks:
dsn: "%messenger.dsn%/pimcore_scheduled_tasks"
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
pimcore_image_optimize:
dsn: "%messenger.dsn%/pimcore_image_optimize"
pimcore_asset_update:
dsn: "%messenger.dsn%/pimcore_asset_update"
pimcore_search_backend_message:
dsn: "%messenger.dsn%/pimcore_search_backend_message"
إذا تستخدم RabbitMQ (AMQP)، كل ناقل ينشئ queue منفصل. إذا تستخدم ناقل Doctrine، كل واحد ينشئ partition جدول منفصل. خطط لنشر العمال وفقاً لذلك.
2.6 تشغيل الترحيلات وإعادة البناء
bin/console doctrine:migrations:migrate
bin/console pimcore:cache:clear
bin/console cache:clear
rm -rf var/cache/*
اختبر كل شي عند نقطة التحقق هاي قبل ما تتقدم.
إذا كنت بتعيد تصميم workflows كمان وقت الترقية، اقرأ دليلنا عن تصميم workflow مؤسسي بـ Pimcore للأنماط المعمارية.
المرحلة 3: الترقية لـ Pimcore 12
3.1 الرخصة وتسجيل المنتج
Pimcore 12 يقدم تغييرين إجباريين:
الرخصة: POCL (Pimcore Open Core License) تحل محل GPLv3. النسخة المجتمعية مع Admin UI Classic Bundle تتطلب رخصة ExtJS (تقريباً 1,480 يورو).
| النسخة | السعر |
|---|---|
| المجتمعية (POCL) | مجاناً (إيرادات أقل من 5 مليون يورو) |
| الاحترافية | 8,400 يورو/سنة |
| المؤسسية ذاتية الاستضافة | 25,200 يورو/سنة |
| المؤسسية PaaS | من 39,900 دولار/سنة |
تسجيل المنتج: إجباري. ثلاث متغيرات بيئة مطلوبة:
| المتغير | الغرض |
|---|---|
PIMCORE_ENCRYPTION_SECRET | مفتاح تشفير Defuse، يُولّد مرة واحدة لكل تثبيت |
PIMCORE_PRODUCT_KEY | مفتاح التسجيل من license.pimcore.com |
PIMCORE_ENTERPRISE_TOKEN | رمز مصادقة Composer للحزم الخاصة |
خطوات التسجيل:
# 1. توليد سر التشفير (مرة واحدة لكل تثبيت، لا تغيره أبداً بعدها)
vendor/bin/generate-defuse-key
# 2. حساب hash التسجيل
php -r "echo hash_hmac('sha256', 'your-instance-id', 'your-encryption-secret');"
# 3. سجّل على license.pimcore.com بالـ hash
# URL: https://license.pimcore.com/register?instance_identifier=ID&instance_hash=HASH
# 4. استلم مفتاح المنتج، عيّنه كمتغير بيئة PIMCORE_PRODUCT_KEY
# config/config.yaml
pimcore:
encryption:
secret: '%env(PIMCORE_ENCRYPTION_SECRET)%'
product_registration:
product_key: '%env(PIMCORE_PRODUCT_KEY)%'
instance_identifier: 'your-instance-id'
التحقق يتم بالكامل offline. يشتغل عند كل بناء لـ Symfony container (مسح كاش، بدء التطبيق). يتحقق من توقيع ECDSA لمفتاح المنتج مقابل مفتاح عام مشحون مع Pimcore. ما يحتاج إنترنت. إذا فشل التحقق، يتم رمي InvalidConfigurationException والتطبيق يرفض يشتغل.
حرج: لا تغير سر التشفير بعد الإعداد الأولي. كل البيانات المشفرة بقاعدة البيانات تعتمد عليه. تعامل معه مثل كلمة مرور قاعدة البيانات. كل تثبيت منفصل (staging، production) يقدر يشارك نفس المفتاح إذا يشاركون نفس معرّف المثيل.
3.2 تحديث Composer
composer require -W pimcore/pimcore:^12.0
3.3 إعدادات الأمان
security:
# احذف (افتراضي بـ Symfony 6.4+):
# enable_authenticator_manager: true
firewalls:
pimcore_admin_webdav:
pattern: ^/asset/webdav # كان: ^/admin/asset/webdav
access_control:
- { path: ^/asset/webdav, roles: ROLE_PIMCORE_USER }
3.4 محرر WYSIWYG
# الخيار أ: TinyMCE (الاحترافية/المؤسسية)
composer require pimcore/tinymce-bundle
# الخيار ب: Quill (مجاني، النسخة المجتمعية)
composer require pimcore/quill-bundle
3.5 تغييرات Doctrine DBAL 4 النهائية
بالإضافة لاستبدال الدوال من المرحلة 1، حدّث إعدادات Doctrine:
# config/packages/doctrine.yaml
doctrine:
dbal:
connections:
default:
driver: "pdo_mysql"
server_version: "mariadb-10.11" # كن محدداً
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_520_ci # الافتراضي لـ P12
Redis بالتفصيل
Redis يخدم أربع وظائف حرجة بنشر Pimcore 12 إنتاجي. الإعدادات الخاطئة تسبب فشل صامت صعب التشخيص.
1. كاش التطبيق (مع دعم Tags)
# config/packages/cache.yaml
framework:
cache:
# Pimcore 10 (قديم)
# pools:
# pimcore.cache.pool:
# tags: true # احذف
# adapter: pimcore.cache.adapter.redis_tag_aware # اسم المحول القديم
# Pimcore 11+/12 (جديد)
pools:
pimcore.cache.pool:
public: true
default_lifetime: 31536000 # سنة واحدة
adapter: cache.adapter.redis_tag_aware # اسم المحول تغير
provider: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%'
تغيير اسم المحول من pimcore.cache.adapter.redis_tag_aware لـ cache.adapter.redis_tag_aware صامت. الاسم القديم ما يرمي خطأ. الكاش بس يرجع بصمت لنظام الملفات، وتتساءل ليش نشرك متعدد الـ pods عنده بيانات قديمة بكل مكان.
بدون كاش Redis، كل عرض صفحة يضرب قاعدة البيانات لأشجار التنقل، إعدادات الموقع، تعريفات الكلاسات، الترجمات، وأكثر. على نشر Kubernetes متعدد الـ pods، كل pod يحتفظ بكاش نظام ملفات خاص فيه بدون تنسيق إبطال. Redis يخلي إبطال الكاش متسق عبر كل الـ pods.
2. تخزين الجلسات
# config/packages/framework.yaml
framework:
session:
handler_id: '%env(REDIS_SESSION_DSN)%'
# مثال: redis://redis:6379/1
للنشر متعدد الـ pods، الجلسات لازم تكون مشتركة. بدون جلسات Redis، المستخدمين ينطردون لما طلبهم يوصل لـ pod مختلف (توزيع حمل round-robin). هالعَرَض الأكثر وضوحاً لإعدادات Redis الناقصة.
3. كاش الصفحة الكاملة
كاش الصفحة الكاملة بـ Pimcore يخزن مخرجات HTML المعروضة للمستخدمين المجهولين. بـ Pimcore 12، يقدر يستخدم Redis كخلفية:
# config/packages/pimcore.yaml
pimcore:
full_page_cache:
enabled: true
lifetime: 7200 # ساعتين
exclude_patterns:
- '/admin'
- '/api'
exclude_cookie: 'pimcore_admin_sid'
كاش الصفحة الكاملة يقلل حمل قاعدة البيانات بشكل كبير للصفحات العامة. صفحة تاخذ 200ms للعرض من قاعدة البيانات تُقدّم بأقل من 5ms من الكاش. على المواقع ذات الحركة العالية، هالفرق بين الحاجة لـ 2 web pods والحاجة لـ 10.
إبطال الكاش يصير تلقائياً لما المحررين ينشرون محتوى. لكن انتبه من المحتوى المخصص: أي صفحة تتغير حسب سياق المستخدم (حالة الدخول، اللغة، متغير A/B testing) لازم تُستثنى من كاش الصفحة الكاملة أو تستخدم ESI fragments.
4. ناقل Messenger (اختياري)
framework:
messenger:
transports:
pimcore_core:
dsn: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%/messages'
Redis Streams يقدر يحل محل RabbitMQ كناقل رسائل. أبسط للتشغيل (خدمة أقل)، لكن يفتقر لـ dead letter queues وأولويات الرسائل وتوجيه exchange. للتثبيتات ذات طوبولوجيات عمال معقدة، RabbitMQ يبقى أفضل. للإعدادات الأبسط، Redis Streams يشتغل تمام.
تحجيم Redis
| حجم Pimcore | ذاكرة Redis | ملاحظات |
|---|---|---|
| صغير (أقل من 10K كائن) | 256 MB | مثيل واحد |
| متوسط (10K-100K كائن) | 1-2 GB | مثيل واحد، التثبيت مفعّل |
| كبير (100K+ كائن) | 4-8 GB | فكّر بـ Redis Sentinel أو Cluster |
راقب used_memory و evicted_keys. إذا المفاتيح عم تنطرد، الكاش صغير وعم تخسر أداء.
ترحيل Collation (الجزء الأصعب)
هالقسم يوفّر أكثر وقت. ترحيل collation هو الجزء الأخطر والأقل توثيقاً بكل الترقية.
ليش ينكسر
ترحيلات Pimcore 12 تنشئ جداول جديدة وتعدّل أعمدة موجودة بـ collation utf8mb4_unicode_520_ci. قاعدة بياناتك الموجودة من Pimcore 10 تستخدم أي شي كان الافتراضي لسيرفر MySQL. على Azure MySQL 8.0، هذا utf8mb4_0900_ai_ci. على إعدادات أقدم، ممكن يكون utf8mb4_general_ci.
النتيجة: collations مختلطة ضمن نفس الجدول وعبر الجداول. لما Pimcore يشغل استعلامات UNION عبر جداول objects و assets و documents:
-- هذا اللي getRelationData() يشغله
SELECT r.dest_id, r.type, o.className as subtype, concat(o.path, o.`key`) as `path`
FROM objects o, object_relations_X r WHERE ...
UNION
SELECT r.dest_id, r.type, a.type as subtype, concat(a.path, a.filename) as `path`
FROM assets a, object_relations_X r WHERE ...
UNION
SELECT r.dest_id, r.type, d.type as subtype, concat(d.path, d.`key`) as `path`
FROM documents d, object_relations_X r WHERE ...
إذا objects.className هو utf8mb4_unicode_520_ci (من ترحيل P12) لكن assets.type هو utf8mb4_0900_ai_ci (من افتراضي Azure)، MySQL يرمي:
SQLSTATE[HY000]: General error: 1267 Illegal mix of collations for operation 'UNION'
هالخطأ يظهر لما تفتح أي DataObject بواجهة الإدارة. النظام عملياً مكسور.
التوزيع الفعلي اللي رح تلاقيه
على قاعدة بيانات تمت ترقيتها من P10 عبر P11 لـ P12 على Azure MySQL 8.0:
| Collation | الجداول | الأعمدة | المصدر |
|---|---|---|---|
utf8mb4_0900_ai_ci | ~580 | ~5,700 | افتراضي Azure MySQL 8.0 |
utf8mb4_unicode_ci | ~87 | ~155 | جداول حزم قديمة |
utf8mb4_unicode_520_ci | ~6 | ~994 | منشأة بترحيل P12 |
utf8mb3_general_ci | ~4 | ~31 | جداول قديمة جداً |
utf8mb4_general_ci | ~1 | ~3 | جداول حزم محددة |
# تحقق من التوزيع الحالي
bin/console doctrine:query:sql "SELECT TABLE_COLLATION, COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION IS NOT NULL GROUP BY TABLE_COLLATION ORDER BY cnt DESC"
# فحص على مستوى الأعمدة (هنا المشكلة الحقيقية تختبئ)
bin/console doctrine:query:sql "SELECT COLLATION_NAME, COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND COLLATION_NAME IS NOT NULL GROUP BY COLLATION_NAME ORDER BY cnt DESC"
خمس حالات خاصة تكسر ALTER TABLE البسيط
1. جداول مفهرسة بالمسار (assets، documents، objects، http_error_log):
الفهارس المركبة على path + key/filename تتجاوز حد فهرس InnoDB البالغ 3072 بايت بعد تحويل collation (utf8mb4 = 4 بايت/حرف، 255 حرف x 4 = 1020 بايت لكل عمود، المركب = فوق الحد).
-- احذف الفهارس، حوّل، أعد الإنشاء بأطوال بادئة
DROP INDEX fullpath ON objects;
DROP INDEX type_path_classId ON objects;
ALTER TABLE objects CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE INDEX fullpath ON objects (path(255), `key`(255));
CREATE INDEX type_path_classId ON objects (type, path(255), classId);
2. جداول classification store: قيد FK بين object_classificationstore_data_* و object_classificationstore_groups_*. قيد FK لازم يُحذف قبل التحويل ويُعاد إنشاؤه بعده. اسم العمود ممكن يكون id (P12) أو o_id (P10/11) حسب حالة الترحيل.
3. جداول الترجمة (translations_admin، translations_messages): تغيير collation يقدر يكشف تكرارات حساسة لحالة الأحرف. إذا عندك "MyKey" و "mykey" كمفاتيح ترجمة منفصلة (صالحة تحت collation حساس لحالة الأحرف)، التحويل لـ collation غير حساس يسبب انتهاك قيد فريد. لقينا ~1,600 تكرار بقاعدة بيانات staging واحدة. التكرارات لازم تُزال قبل التحويل.
4. جدول الأصول: لازم تُعيد إنشاء فهرس fullpath كغير فريد. أسماء ملفات مختلفة بحالة الأحرف يمكن تكون موجودة (مثلاً EEVA.tif و Eeva.tif ملفات مختلفة شرعية). فهرس فريد رح يرفض الثاني بعد تغيير collation.
5. الجداول الكبيرة: جداول بملايين الصفوف (جداول الاستعلام المترجمة مثلاً، يمكن يكون 30+ جدول لكل كلاس مع واحد لكل لغة) تاخذ وقت كبير لعمل ALTER. على قاعدة بيانات فيها 680 جدول، التحويل الكامل ياخذ ~18 دقيقة.
تشغيل الترحيل
شغّله خلال نافذة صيانة مع تقليص web pods:
# قلّص لتجنب تنازع الأقفال على جداول الترجمة
kubectl scale deployment pimcore pimcore-frontend -n <namespace> --replicas=0
kubectl get pods -n <namespace> -l app=pimcore -w # انتظر الإنهاء
بعد تشغيل سكربت الترحيل الآلي (اللي يتعامل مع كل الحالات الخمس الخاصة):
# أعد بناء تعريفات الكلاسات (يستعيد قيود FK)
PIMCORE_CLASS_DEFINITION_WRITABLE=1 bin/console pimcore:deployment:classes-rebuild -c
# تحقق إنه بقي collation واحد فقط
bin/console doctrine:query:sql "SELECT TABLE_COLLATION, COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION IS NOT NULL GROUP BY TABLE_COLLATION"
# أعد التوسيع
kubectl scale deployment pimcore -n <namespace> --replicas=1
kubectl scale deployment pimcore-frontend -n <namespace> --replicas=3
مرجع سريع: أخطاء Collation
| الخطأ | السبب | الحل |
|---|---|---|
Illegal mix of collations for operation 'UNION' | collations مختلطة عبر الجداول باستعلامات العلاقات | حوّل كل الجداول لـ utf8mb4_unicode_520_ci |
Specified key was too long; max key length is 3072 bytes | فهرس مركب path+key يتجاوز الحد | احذف الفهرس، حوّل، أعد الإنشاء بأطوال بادئة |
Referencing column and referenced column are incompatible | FK بين جداول بـ collations مختلفة | احذف FK، حوّل الجدولين، أعد بناء الكلاسات |
Duplicate entry for key 'PRIMARY' | تغيير collation يخلي مفاتيح مختلفة بالحالة متطابقة | أزل التكرارات قبل التحويل |
Lock wait timeout exceeded | التطبيق يحتفظ بأقفال على جداول الترجمة | قلّص الـ pods قبل ALTER |
تحسين قاعدة البيانات بعد الترحيل
بعد ترحيل collation، شغّل OPTIMIZE على كل الجداول لإزالة التجزئة وإعادة بناء الفهارس:
# الجداول الأساسية
bin/console doctrine:query:sql "OPTIMIZE TABLE assets, documents, objects, versions, translations_admin, translations_messages, search_backend_data, properties, dependencies"
# تحقق من التجزئة العالية
bin/console doctrine:query:sql "SELECT TABLE_NAME, TABLE_ROWS, ROUND(DATA_LENGTH/1024/1024,2) AS data_mb, ROUND(DATA_FREE/1024/1024,2) AS free_mb, ROUND(DATA_FREE/(DATA_LENGTH+1)*100,1) AS frag_pct FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND DATA_FREE > DATA_LENGTH * 0.1 AND DATA_LENGTH > 1048576 ORDER BY DATA_FREE DESC"
إعدادات MySQL SSL
لخدمات MySQL المُدارة (Azure، AWS RDS) مع require_secure_transport=ON:
# config/packages/prod/doctrine.yaml
doctrine:
dbal:
connections:
default:
options:
!php/const PDO::MYSQL_ATTR_SSL_CA: true
!php/const PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false
MYSQL_ATTR_SSL_CA: true يفعّل SSL بدون تحديد مسار شهادة. السيرفر يقدم شهادته والعميل يثق فيها. MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false يتخطى التحقق من اسم المضيف (آمن للشبكات الداخلية).
لكيف نتعامل مع ترحيل قواعد البيانات ضمن ممارسة هندسة البيانات الأوسع، هالصفحة تغطي منهجيتنا.
تكامل OpenSearch
Pimcore 12 يقدم حزمة Generic Data Index، اللي تستخدم OpenSearch (أو Elasticsearch) للبحث الخلفي وفهرسة البيانات.
إعداد الفهارس (لازم يصير قبل الترحيلات)
# أنشئ فهارس فارغة يدوياً (مطلوب قبل ما الترحيلات تشتغل)
curl -X PUT "http://opensearch-cluster:9200/pimcore_document" -H 'Content-Type: application/json' -d '{}'
curl -X PUT "http://opensearch-cluster:9200/pimcore_asset" -H 'Content-Type: application/json' -d '{}'
# لا تنشئ فهارس إحصائيات Portal Engine يدوياً
# تُنشأ تلقائياً بتسمية زمنية: {name}__{year}_{month}
# الإنشاء اليدوي يتعارض مع نمط الـ alias
هالشي حرج لأنه بعض ترحيلات P12 تطلق Document->save()، اللي يطلق حدث DocumentIndexUpdateSubscriber. إذا الفهرس ما موجود: index_not_found_exception يتبعها أخطاء SAVEPOINT متتالية تفسد حالة الترحيل.
بعد ما الترحيلات تكتمل:
# أنشئ فهارس لكل كلاسات DataObject
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
# الأوامر المتاحة
bin/console list generic-data-index
# generic-data-index:deployment:reindex ينشئ/يحدّث فهارس لـ classDefinitions
# generic-data-index:reindex يطلق إعادة فهرسة أصلية
# generic-data-index:update:index يحدّث الفهرس/التعيين
تحجيم OpenSearch Cluster
| حجم التثبيت | العُقد | الذاكرة/عقدة | التخزين/عقدة | Java Heap |
|---|---|---|---|---|
| صغير (أقل من 10K كائن) | 1 | 4 GB | 50 GB SSD | 2 GB |
| متوسط (10K-100K) | 2 | 8 GB | 100 GB SSD | 4 GB |
| كبير (100K+) | 3 (مؤهلة للـ master) | 16 GB | 200 GB SSD | 8 GB |
إذا تستبدل Elasticsearch 7.x، OpenSearch هو بديل مباشر. الـ API متوافق. بس مكتبة العميل ومسارات الإعدادات تتغير. لكيف نصمم بنية البحث بشكل أوسع، شوف دليلنا عن منصات التجارة الإلكترونية.
أمان OpenSearch
للتواصل الداخلي للـ cluster (pods بنفس namespace بـ Kubernetes)، تقدر تعطّل إضافة الأمان:
# opensearch-cluster.yaml
additionalConfig:
plugins.security.disabled: "true"
للوصول الخارجي أو clusters متعددة المستأجرين، اعدّ مصادقة صحيحة.
أخطاء Flysystem والتخزين البعيد
إذا تستخدم تخزين أصول بعيد (Azure Blob، AWS S3، Google Cloud Storage)، Pimcore 12 عنده خطأين لازم تعرفهم.
الخطأ 1: getDimensions() تحمّل كل صورة عند كل عرض صفحة
يأثر على: Pimcore 10.6+، 11.x، 12.x مع تخزين Flysystem بعيد
كل استدعاء thumbnail()->html() يطلق getDimensions()، اللي يستدعي readDimensionsFromFile() قبل ما يجرب getEstimatedDimensions(). على التخزين البعيد، كل قراءة ملف تكلف 50-100ms من I/O الشبكة.
التأثير المقاس: صفحة فيها ~80 مرجع صورة أخذت 5,735ms (79 استدعاء تخزين بعيد بمعدل ~65ms لكل واحد). بعد تصليح ترتيب الدوال، نفس الصفحة اتعرضت بـ 170ms مع صفر استدعاءات بعيدة.
السبب الجذري بـ ImageThumbnailTrait::getDimensions():
// الترتيب الحالي (خاطئ للتخزين البعيد)
// الخطوة 1: تحقق من كاش thumbnail بقاعدة البيانات (سريع، لكن غالباً miss)
// الخطوة 2: readDimensionsFromFile() → تحمّل الملف من الـ CLOUD (~65ms)
// الخطوة 3: getEstimatedDimensions() → ما توصلها أبداً (الخطوة 2 دائماً تنجح)
// الترتيب المصلّح
if (empty($dimensions) && $config && $asset instanceof Image) {
$dimensions = $config->getEstimatedDimensions($asset); // ~0ms، حساب رياضي صرف
}
if (empty($dimensions) && $this->exists()) {
$dimensions = $this->readDimensionsFromFile(); // 50-100ms، آخر ملجأ فقط
}
getEstimatedDimensions() تحسب الأبعاد من أبعاد الصورة الأصلية (مخزنة بقاعدة البيانات) وإعدادات تحويل thumbnail. حساب رياضي صرف، صفر I/O. بالإنتاج، 95%+ من الصور عندها أبعاد مخزنة. هالإصلاح يزيل تقريباً كل I/O التخزين البعيد خلال عرض الصفحات.
الخطأ 2: صور المعاينة للمجلدات عالقة على دائرة التحميل
يأثر على: admin-ui-classic-bundle 2.2.x، 2.3.x
بعد رفع صور، تبويب المعاينة يعرض دائرة تحميل ما تنتهي أبداً. المتصفح يخزن placeholder GIF بشكل دائم لأنه الرابط يستخدم modificationDate كـ cache buster (ما يتغير أبداً)، وما في توليد thumbnail غير متزامن يتم إرساله لـ folderPreview origin (بعكس treeNode اللي يرسله).
الحل: أضف Cache-Control: no-store لاستجابة placeholder وأرسل AssetPreviewImageMessage.
نشر Kubernetes
بنية الـ Pods
| الـ Pod | الغرض | النسخ |
|---|---|---|
pimcore-web | Nginx + PHP-FPM، إدارة + واجهة أمامية | 2-4 |
pimcore-worker | مستهلكي Symfony Messenger | 1-3 |
pimcore-ops | Pod CLI للصيانة (ترحيلات، إعادة بناء) | 1 |
redis | كاش، جلسات، ناقل اختياري | 1+ |
mysql | قاعدة بيانات (أو خدمة مُدارة) | 1+ |
opensearch | فهارس البحث | 2-3 |
rabbitmq | وسيط رسائل (إذا مش مستخدم Redis) | 1-3 |
إدارة الأسرار
apiVersion: v1
kind: Secret
metadata:
name: pimcore-secrets
type: Opaque
data:
PIMCORE_ENCRYPTION_SECRET: <base64>
PIMCORE_PRODUCT_KEY: <base64>
PIMCORE_ENTERPRISE_TOKEN: <base64>
DATABASE_HOST: <base64>
DATABASE_NAME: <base64>
DATABASE_USER: <base64>
DATABASE_PASSWORD: <base64>
REDIS_HOST: <base64>
REDIS_PORT: <base64>
APP_SECRET: <base64>
استخدم Sealed Secrets (Bitnami) أو مشغلات أسرار خارجية للإنتاج. لا تعمل commit لأسرار نصية بـ Git أبداً.
كل init container (إنشاء الأصول، إنشاء الكلاسات، ترحيل قاعدة البيانات، مسح الكاش) لازم يكون عنده كل secretRefs بقائمة envFrom. الأسرار الناقصة تسبب أخطاء Environment variable not found صامتة.
نشر العمال
spec:
replicas: 2
template:
spec:
containers:
- name: worker
command: ["php", "bin/console", "messenger:consume",
"pimcore_core", "pimcore_maintenance", "pimcore_scheduled_tasks",
"pimcore_image_optimize", "pimcore_asset_update",
"pimcore_search_backend_message",
"--time-limit=3600", "--memory-limit=512M"]
--time-limit و --memory-limit يضمنون إنه العمال يعيدون التشغيل دورياً، يمنعون تسريبات ذاكرة PHP. Kubernetes يعيد تشغيل الـ pod لما العملية تنتهي بسلاسة.
ترتيب النشر
1. انشر موارد Kubernetes
2. ادخل shell على ops pod
3. ثبّت الحزم الناقصة (PimcoreAdminBundle، GenericExecutionEngine، الخ.)
4. أنشئ فهارس OpenSearch يدوياً (pimcore_document، pimcore_asset)
5. شغّل إعادة فهرسة DataObject بـ OpenSearch
6. شغّل ترحيلات قاعدة البيانات (مع PIMCORE_CLASS_DEFINITION_WRITABLE=1)
7. شغّل ترحيل collation (نافذة صيانة، web pods مقلّصة)
8. شغّل تحسين قاعدة البيانات (OPTIMIZE TABLE)
9. أوامر ما بعد النشر (classes-rebuild، reindex، assets:install، cache:clear)
10. تحقق: افتح DataObject بالإدارة، تحقق من الأصول، تحقق من الواجهة الأمامية
خدمات الكلاود تغطي نشر وتشغيل Kubernetes لـ Pimcore ومنصات ثانية.
بنية نظام الأحداث بـ Pimcore 12
فهم نظام الأحداث بـ Pimcore حرج للترقية ولبناء الإضافات.
أحداث PHP (الخلفية)
Pimcore يستخدم EventDispatcher من Symfony. الأحداث الرئيسية لـ DataObjects:
use Pimcore\Event\DataObjectEvents;
// الأحداث المتاحة
DataObjectEvents::PRE_ADD // قبل أول حفظ
DataObjectEvents::POST_ADD // بعد أول حفظ
DataObjectEvents::PRE_UPDATE // قبل كل حفظ
DataObjectEvents::POST_UPDATE // بعد كل حفظ
DataObjectEvents::PRE_DELETE // قبل الحذف
DataObjectEvents::POST_DELETE // بعد الحذف
إنشاء event subscriber بـ Pimcore 12:
use Pimcore\Event\Model\DataObjectEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
DataObjectEvents::POST_UPDATE => 'onPostUpdate',
];
}
public function onPostUpdate(DataObjectEvent $event): void
{
$object = $event->getObject();
if (!$object instanceof Product) {
return;
}
// فهرسة للبحث، توليد أصول، الخ.
}
}
أحداث JavaScript (واجهة الإدارة)
Pimcore 12 يستخدم نمط اشتراك أحداث مختلف عن P10:
// أحداث إدارة Pimcore 12
document.addEventListener(pimcore.events.preOpenObject, (e) => {
// قبل فتح محرر الكائن
});
document.addEventListener(pimcore.events.postOpenObject, (e) => {
// بعد فتح محرر الكائن، e.detail.object متاح
});
document.addEventListener(pimcore.events.preSaveObject, (e) => {
// قبل الحفظ، يمكن الإلغاء بـ e.preventDefault()
});
document.addEventListener(pimcore.events.postSaveObject, (e) => {
// بعد اكتمال الحفظ
});
توسيع Pimcore 12
الحزم المخصصة بـ Pimcore 12 تتبع اتفاقيات حزم Symfony:
// src/CustomBundle/CustomBundle.php
namespace App\CustomBundle;
use Pimcore\Extension\Bundle\AbstractPimcoreBundle;
class CustomBundle extends AbstractPimcoreBundle
{
public function getJsPaths(): array
{
return [
'/bundles/custom/js/startup.js',
];
}
public function getCssPaths(): array
{
return [
'/bundles/custom/css/admin.css',
];
}
}
سجّل JavaScript و CSS المخصص للإدارة بإعدادات الحزمة:
# config/packages/pimcore_admin.yaml
pimcore_admin:
assets:
js:
- '/bundles/custom/js/startup.js'
- '/bundles/custom/js/grid-operators.js'
css:
- '/bundles/custom/css/admin.css'
التحقق بعد الترقية
تسلسل الأوامر
PIMCORE_CLASS_DEFINITION_WRITABLE=1 php -d memory_limit=2G bin/console pimcore:deployment:classes-rebuild -c
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
bin/console assets:install public
php -d memory_limit=2G bin/console pimcore:search-backend-reindex
bin/console messenger:setup-transports
bin/console cache:clear
bin/console cache:warmup
مصفوفة الاختبار الوظيفي
| المجال | شو تختبر | كيف تتحقق |
|---|---|---|
| دخول الإدارة | دخول، مصادقة ثنائية، خروج | يدوي بالمتصفح |
| DataObjects | إنشاء، تعديل، حفظ، نشر، حذف، إصدارات | افتح أي كائن، عدّل حقل، احفظ |
| الأصول | رفع، صور مصغرة، بيانات وصفية، تحميل | ارفع صورة، تحقق من توليد الصور المصغرة |
| المستندات | تعديل صفحات، areabricks، حفظ، نشر | عدّل صفحة فيها areabricks |
| العلاقات | علاقات كائنات، علاقات أصول | افتح كائن فيه علاقات، تحقق إنه استعلام UNION يشتغل |
| البحث | بحث خلفي، فهرسة OpenSearch | ابحث عن كائن بشريط أدوات الإدارة |
| Messenger | معالجة الطوابير، كل الناقلات | تحقق من bin/console messenger:stats |
| الكاش | اتصال Redis، الإبطال | انشر محتوى، تحقق إنه التغيير ظاهر على pod ثاني |
| كاش الصفحة الكاملة | تخزين الصفحات المجهولة، الإبطال | حمّل صفحة، تحقق من ترويسات الاستجابة لـ cache hit |
| تكامل ERP | معالجة الاستيراد، تعيين الحقول | شغّل استيراد تجريبي |
| الصلاحيات | RBAC، صلاحيات workflow | سجّل دخول كمستخدم مقيد، تحقق من الوصول |
| JS المخصص | مشغلات grid، إضافات الإدارة | افتح عرض grid، تحقق إنه الأعمدة المخصصة تشتغل |
معايير الأداء
| المقياس | مقبول | تحذير | حرج |
|---|---|---|---|
| تحميل صفحة الإدارة | أقل من 2 ثانية | 2-5 ثانية | أكثر من 5 ثانية |
| فتح DataObject | أقل من 3 ثانية | 3-8 ثانية | أكثر من 8 ثانية |
| رفع أصل (10MB) | أقل من 5 ثانية | 5-15 ثانية | أكثر من 15 ثانية |
| استجابة البحث | أقل من 500ms | 500ms-2 ثانية | أكثر من 2 ثانية |
| صفحة واجهة أمامية (مخزنة) | أقل من 50ms | 50-200ms | أكثر من 200ms |
| صفحة واجهة أمامية (غير مخزنة) | أقل من 500ms | 500ms-2 ثانية | أكثر من 2 ثانية |
| عرض صورة مصغرة (تخزين بعيد) | أقل من 200ms | 200ms-1 ثانية | أكثر من 1 ثانية (تحقق من خطأ getDimensions) |
الأخطاء الشائعة
-
تخطي Pimcore 11. ما تقدر تقفز من 10 لـ 12. الإصدار الوسيط يتعامل مع تغييرات schema حرجة واستخراج الحزم وإزالة البادئة.
-
تشغيل الترحيلات بدون فهارس OpenSearch. بعض الترحيلات تطلق event subscribers يستعلمون OpenSearch.
index_not_found_exceptionيتسلسل لحالة SAVEPOINT فاسدة. أنشئ الفهارس أولاً. -
عدم تصليح collations. الـ collations المختلطة تسبب فشل استعلامات UNION لما تفتح أي DataObject. هالشي يكسر واجهة الإدارة بصمت لين محرر يفتح منتج.
-
ضياع سر التشفير. إذا ضاع بعد النشر، كل البيانات المشفرة بقاعدة البيانات تصير غير قابلة للاسترداد. هالشي مش قابل للاسترداد. تعامل معه مثل كلمة مرور قاعدة البيانات الرئيسية.
-
التراجع الصامت لكاش Redis. تغيير اسم المحول من
pimcore.cache.adapter.redis_tag_awareلـcache.adapter.redis_tag_awareما يرمي أخطاء. الكاش بصمت يرجع لنظام الملفات. النشر متعدد الـ pods ينكسر. -
عدم تحديث ناقلات Messenger. الناقلات الناقصة لـ P11 تسبب توقف صامت للمهام المجدولة ومعالجة الأصول وفهرسة البحث.
-
تشغيل ترحيل collation والـ web pods شغالة. تحويل جداول الترجمة يسبب deadlock تحت الوصول المتزامن. قلّص أولاً.
-
إنشاء فهارس إحصائيات Portal Engine يدوياً. الإنشاء التلقائي يستخدم أسماء فهارس زمنية مع aliases. الإنشاء اليدوي يتعارض مع نمط الـ alias.
-
الاختبار على قاعدة بيانات فارغة. ترحيل schema على قاعدة بيانات فارغة يخفي 90% من المشاكل. استخدم نسخة بيانات إنتاج كاملة.
-
تجاهل خطأ getDimensions() على التخزين السحابي. كل عرض صفحة غير مخزنة يكون أبطأ بـ 3-7 ثواني من اللازم لين يتم التصحيح. الإصلاح هو إعادة ترتيب سطرين.
-
نقص
getSupportedTypes()على normalizers. Symfony 7.x يتطلب هالدالة. عدم وجودها يسبب خطأ fatal، مش تحذير إهمال. -
عدم تحديث استدعاءات
Request::get(). مهملة لكن لسا تشتغل بـ Symfony 6.x. رح تنكسر بـ 7.x. -
نسيان
PIMCORE_CLASS_DEFINITION_WRITABLE=1. الترحيلات وإعادة بناء الكلاسات تفشل بصمت بدون متغير البيئة هذا. رسالة الخطأ واضحة، لكن من السهل تنساها. -
ExtJS 7
rootمقابلrootProperty. Store readers المخصصة اللي تستخدمroot: 'data'تفشل بصمت بتحليل الاستجابات بـ ExtJS 7.
النقاط الرئيسية
-
ثلاث مراحل، بدون اختصارات. التحضير (صلّح كل الكود المهمل)، Pimcore 11 (ترقية الإطار + استخراج الحزم)، Pimcore 12 (التسجيل، DBAL 4، collation، نظام الأحداث). كل مرحلة نقطة تحقق.
-
ترحيل collation هو الجزء الأصعب. خمسة أنواع من الجداول الخاصة تحتاج معالجة مخصصة. احجز نافذة صيانة. استخدم سكربت آلي. تحقق على مستوى الجدول والعمود.
-
تسجيل المنتج إجباري و offline. ما يحتاج إنترنت للتحقق. لكن سر التشفير غير قابل للاسترداد إذا ضاع.
-
Redis مش اختياري بالنشر متعدد الـ pods. اتساق الكاش، مشاركة الجلسات، كاش الصفحة الكاملة، واختيارياً ناقل الرسائل كلهم يعتمدون عليه. تغيير اسم المحول هو تغيير جذري صامت.
-
فهارس OpenSearch لازم تكون موجودة قبل الترحيلات. أنشئ فهارس المستندات والأصول يدوياً، بعدين شغّل إعادة فهرسة data object. الترتيب مهم.
-
ترحيل نظام الأحداث ميكانيكي لكن حرج. Plugin Broker لـ Event Listeners،
o_classNameلـclassName،@Routeلـ#[Route]،Request::get()لـ accessors محددة. Rector يقدر يأتمت 80% منها. -
التخزين البعيد يكشف أخطاء Pimcore. خطأ ترتيب getDimensions() يضيف ثواني لكل عرض صفحة. خطأ كاش معاينة المجلدات يخلي الصور المصغرة عالقة للأبد. الاثنين عندهم حلول بسيطة.
-
اختبر على نسخة بيانات إنتاج. كل مرة. ترقية قاعدة بيانات فارغة تاخذ 20 دقيقة وتنجح بسهولة. المشاكل الحقيقية تظهر مع 70K أصل، 680 جدول بـ collations مختلطة، و 50K+ data objects.
هالنوع بالضبط من أعمال الهندسة البرمجية اللي فريقنا يتعامل معها بانتظام. إذا عم تخطط لترقية Pimcore، فريق الاستشارات عنا سوّى هالشي لعدة مشاريع مؤسسية عبر منطقة DACH. كمان نقدمها كجزء من خدماتنا الأوسع.
جاهز تبدأ ترقية Pimcore؟ تواصل مع فريقنا أو اطلب عرض سعر.
المواضيع المغطاة
أدلة ذات صلة
الدليل الشامل لأنظمة الذكاء الاصطناعي الوكيلي
دليل تقني لأنظمة الذكاء الاصطناعي الوكيلي في بيئات الأعمال. تعرف على البنية والقدرات والتطبيقات العملية للوكلاء المستقلين.
اقرأ الدليلالتجارة الوكيلية: كيف تخلي وكلاء الذكاء الاصطناعي يشترون بأمان
كيف تصمم تجارة وكيلية محكومة. محركات السياسات، بوابات الموافقة البشرية، إيصالات HMAC، الـ idempotency، عزل المستأجرين، وبروتوكول الدفع الوكيلي الكامل.
اقرأ الدليلالـ 9 أماكن اللي نظام AI تبعك بيسرّب بيانات منها (وكيف تسد كل وحدة)
خارطة منهجية لكل مكان البيانات بتتسرب منه بأنظمة AI. البرومبتات، الـ embeddings، السجلات، استدعاءات الأدوات، ذاكرة الـ agent، رسائل الأخطاء، الكاش، بيانات التدريب، وتسليمات الـ agents.
اقرأ الدليلجاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟
فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.
ابدأ محادثة