دليل تقني

ترقية Pimcore من 10 لـ 12: مسار الترحيل الحقيقي

الدليل الشامل لترقية Pimcore 10 لـ 12. PHP 8.3، Symfony 7، DBAL 4، ترحيل collation، نظام الأحداث، ExtJS 7، Redis، OpenSearch، Kubernetes، Flysystem، وكل الأخطاء اللي ما لقيناها بالتوثيق.

20 مارس 202645 دقيقة للقراءةفريق هندسة أورنتس

ليش ما تقدر تتخطى Pimcore 11

أول سؤال كل فريق يسأله: نقدر نقفز مباشرة من 10 لـ 12؟ لا. Pimcore 11 هو المحطة الإجبارية. التغييرات بالإطار كبيرة جداً تتشرّب بخطوة وحدة، وأدوات الترحيل من Pimcore نفسها تفترض إنه الإصدار الوسيط موجود.

هاي الصورة الكاملة لشو يتغير عبر الإصدارين الرئيسيين:

المكوّنPimcore 10.6Pimcore 11Pimcore 12
PHP8.0+8.1+8.3+
Symfony5.46.2+6.4 / 7.x
Doctrine DBAL2.x / 3.x3.x4.x
واجهة الإدارةمدمجة (ExtJS 6)حزمة منفصلة (ExtJS 6/7)حزمة منفصلة (ExtJS 7) + Studio UI (React)
الرخصةGPLv3GPLv3POCL (Pimcore Open Core License)
الحزم الأساسيةمتراصةمستخرجةمستخرجة
محرك البحثمدمجSimple Backend Search BundleGeneric Data Index + OpenSearch
محرر WYSIWYGTinyMCE مدمجحزمة TinyMCEحزمة TinyMCE أو Quill
تسجيل المنتجمش مطلوبمش مطلوبمطلوب (تحقق offline)
نظام الأحداث (JS)Plugin BrokerEvent ListenersEvent Listeners + أحداث Studio UI
Collationutf8mb4_general_ciutf8mb4_general_ciutf8mb4_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() مع DELETESQL يدوي
$db->updateWhere()$db->executeStatement() مع UPDATESQL يدوي
$db->quote($val, $type)$db->quote($val)سلاسل نصية فقط بـ DBAL 4، بدون معامل النوع
Pimcore\Db\ConnectionInterfaceDoctrine\DBAL\Connectionتغيير type hint بـ DI
Pimcore\Db\ConnectionDoctrine\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_idid (أو oo_id بجداول object store)المفتاح الرئيسي
o_creationDatecreationDateالطابع الزمني
o_modificationDatemodificationDateالطابع الزمني
o_pathpathمسار الشجرة
o_keykeyمفتاح/slug الكائن
o_publishedpublishedعلم النشر
o_parentIdparentIdمرجع الأب
o_typetypeنوع الكائن
o_classNameclassNameاسم الكلاس
o_classIdclassIdمعرّف الكلاس
o_userOwneruserOwnerمعرّف المستخدم المنشئ
o_userModificationuserModificationمعرّف آخر معدّل
o_versionCountversionCountعداد الإصدارات
# ابحث عن كل استخدام بادئة 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 بـ JSclassNameيطابق تغيير 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 incompatibleFK بين جداول بـ 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 كائن)14 GB50 GB SSD2 GB
متوسط (10K-100K)28 GB100 GB SSD4 GB
كبير (100K+)3 (مؤهلة للـ master)16 GB200 GB SSD8 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-webNginx + PHP-FPM، إدارة + واجهة أمامية2-4
pimcore-workerمستهلكي Symfony Messenger1-3
pimcore-opsPod 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 ثانية
استجابة البحثأقل من 500ms500ms-2 ثانيةأكثر من 2 ثانية
صفحة واجهة أمامية (مخزنة)أقل من 50ms50-200msأكثر من 200ms
صفحة واجهة أمامية (غير مخزنة)أقل من 500ms500ms-2 ثانيةأكثر من 2 ثانية
عرض صورة مصغرة (تخزين بعيد)أقل من 200ms200ms-1 ثانيةأكثر من 1 ثانية (تحقق من خطأ getDimensions)

الأخطاء الشائعة

  1. تخطي Pimcore 11. ما تقدر تقفز من 10 لـ 12. الإصدار الوسيط يتعامل مع تغييرات schema حرجة واستخراج الحزم وإزالة البادئة.

  2. تشغيل الترحيلات بدون فهارس OpenSearch. بعض الترحيلات تطلق event subscribers يستعلمون OpenSearch. index_not_found_exception يتسلسل لحالة SAVEPOINT فاسدة. أنشئ الفهارس أولاً.

  3. عدم تصليح collations. الـ collations المختلطة تسبب فشل استعلامات UNION لما تفتح أي DataObject. هالشي يكسر واجهة الإدارة بصمت لين محرر يفتح منتج.

  4. ضياع سر التشفير. إذا ضاع بعد النشر، كل البيانات المشفرة بقاعدة البيانات تصير غير قابلة للاسترداد. هالشي مش قابل للاسترداد. تعامل معه مثل كلمة مرور قاعدة البيانات الرئيسية.

  5. التراجع الصامت لكاش Redis. تغيير اسم المحول من pimcore.cache.adapter.redis_tag_aware لـ cache.adapter.redis_tag_aware ما يرمي أخطاء. الكاش بصمت يرجع لنظام الملفات. النشر متعدد الـ pods ينكسر.

  6. عدم تحديث ناقلات Messenger. الناقلات الناقصة لـ P11 تسبب توقف صامت للمهام المجدولة ومعالجة الأصول وفهرسة البحث.

  7. تشغيل ترحيل collation والـ web pods شغالة. تحويل جداول الترجمة يسبب deadlock تحت الوصول المتزامن. قلّص أولاً.

  8. إنشاء فهارس إحصائيات Portal Engine يدوياً. الإنشاء التلقائي يستخدم أسماء فهارس زمنية مع aliases. الإنشاء اليدوي يتعارض مع نمط الـ alias.

  9. الاختبار على قاعدة بيانات فارغة. ترحيل schema على قاعدة بيانات فارغة يخفي 90% من المشاكل. استخدم نسخة بيانات إنتاج كاملة.

  10. تجاهل خطأ getDimensions() على التخزين السحابي. كل عرض صفحة غير مخزنة يكون أبطأ بـ 3-7 ثواني من اللازم لين يتم التصحيح. الإصلاح هو إعادة ترتيب سطرين.

  11. نقص getSupportedTypes() على normalizers. Symfony 7.x يتطلب هالدالة. عدم وجودها يسبب خطأ fatal، مش تحذير إهمال.

  12. عدم تحديث استدعاءات Request::get(). مهملة لكن لسا تشتغل بـ Symfony 6.x. رح تنكسر بـ 7.x.

  13. نسيان PIMCORE_CLASS_DEFINITION_WRITABLE=1. الترحيلات وإعادة بناء الكلاسات تفشل بصمت بدون متغير البيئة هذا. رسالة الخطأ واضحة، لكن من السهل تنساها.

  14. 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؟ تواصل مع فريقنا أو اطلب عرض سعر.

المواضيع المغطاة

ترقية Pimcoreترحيل Pimcore 12ترقية Pimcore 11ترحيل Symfony 7ترقية PHP 8.3Doctrine DBAL 4Pimcore KubernetesPimcore OpenSearchPimcore RedisPimcore full page cacheExtJS 7Pimcore Studio UIنظام أحداث Pimcore

جاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟

فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.

ابدأ محادثة