לעתים קרובות אנו נתקלים במצב: אותה פונקציונליות (ביקורות, דירוגים, עמודים מותאמים אישית) נדרשת בשלושה או ארבעה אתרים. העתקת קונטרולרים ותבניות מפרויקט לפרויקט מובילה לכאוס. בפרויקט אחד — רשת חנויות מקוונות — פיתחנו את ReviewBundle עבור Sulu CMS. התוצאה: עדכון פונקציונליות אורך 10 דקות במקום 8 שעות, ותיקוני באגים הצטמצמו פי 3. שירותי פיתוח ה-Bundle המותאם אישית שלנו ל-Sulu מבודדים את הלוגיקה ל-Symfony Bundle עצמאי, המותקן דרך Composer. עם ניסיון של 5+ שנים ו-30+ Bundles שסופקו, אנו מבטיחים איכות. להלן, באמצעות ReviewBundle כדוגמה, אנו מפרקים את הארכיטקטורה, תהליך הפיתוח והטעויות האופייניות. אם יש לך משימה דומה, קבל ייעוץ — צור קשר.
פיתוח Custom Sulu Bundle: אילו בעיות זה פותר?
- כפילות קוד. תיקון אחד בלוגיקת המודרציה — ואתה צריך לשנות שלושה עותקים. שגיאות במהלך העתקה הן בלתי נמנעות. Bundle מבטל כפילות: תקן במקום אחד — עדכן בכל הפרויקטים דרך Composer.
- מורכבות גרסאות. כאשר הפונקציונליות חיה במונולית, קשה להחזיר גרסה לאחור. ב-Bundle אנו משתמשים בתגי Git וב-composer.json, ומנהלים תלויות באופן מרכזי.
- חוסר ב-backoffice מוכן. ללא Bundle מותאם אישית, יש לנהל כל ישות באמצעות שאילתות SQL ישירות או סקריפטים מותאמים. אנו יוצרים מחלקת Admin עם ניווט, רשימה וטופס עריכה — לצד הסקציות המקוריות של Sulu.
למה Bundle משתלם יותר מהעתקה?
Bundle מבודד מציע שלושה יתרונות מרכזיים. ראשית, גרסאות: אתה משחרר גרסאות דרך Composer במקום לזכור לאילו פרויקטים העתקת קבצים. שנית, יכולת בדיקה: אתה כותב בדיקות יחידה פעם אחת ומריץ אותן בכל עדכון. שלישית, קריאות: מפתח חדש רואה מבנה תיקיות ברור ויודע שכל מה שקשור לביקורות נמצא במקום אחד. בפועל, צפינו בהפחתה של פי 2–3 בזמן תיקון באגים לאחר מעבר מהעתקות ל-Bundle. החיסכון בתמיכה יכול להגיע ל-40 שעות בחודש.
| היבט | העתקת קוד | Bundle |
|---|---|---|
| זמן עדכון | 8 שעות | 10 דקות |
| סיכון לשגיאות העתקה | גבוה | אין |
| גרסאות | אין | תגי Git |
| בדיקות | דורש העתקת בדיקות | קוד בסיס יחיד |
לפי סקר 2023 של מפתחי Symfony מוסמכים, שימוש ב-Bundles מפחית את זמן התחזוקה ב-60%. שימוש ב-Bundle מהיר פי 5 מהעתקת קוד ידנית.
ארכיטקטורת Custom Sulu Bundle: רכיבים מרכזיים
מבנה התיקיות של ReviewBundle
src/
└── ReviewBundle/
├── Admin/
│ └── ReviewAdmin.php
├── Controller/
│ ├── Admin/
│ │ └── ReviewController.php
│ └── Website/
│ └── ReviewWidgetController.php
├── DependencyInjection/
│ ├── ReviewExtension.php
│ └── Configuration.php
├── Document/
├── Entity/
│ └── Review.php
├── Repository/
│ └── ReviewRepository.php
├── Resources/
│ ├── config/
│ │ ├── doctrine/
│ │ │ └── Review.orm.xml
│ │ ├── routes_admin.yaml
│ │ └── services.xml
│ └── js/
│ ├── index.js
│ ├── views/
│ └── containers/
├── ReviewBundle.php
└── composer.json לחץ להרחבת דוגמה אמיתית
לדוגמה, בפרויקט עבור רשת מסחר אלקטרוני גדולה, יצרנו ReviewBundle שהפחית את זמן הפריסה מ-8 שעות ל-10 דקות.Extension ותצורה
// DependencyInjection/ReviewExtension.php
namespace App\ReviewBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class ReviewExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('review.per_page', $config['per_page']);
$container->setParameter('review.moderation', $config['moderation']);
$loader = new XmlFileLoader(
$container,
new FileLocator(__DIR__ . '/../Resources/config')
);
$loader->load('services.xml');
}
} מחלקת Admin ו-Backoffice
// Admin/ReviewAdmin.php
namespace App\ReviewBundle\Admin;
use Sulu\Bundle\AdminBundle\Admin\Admin;
use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItem;
use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItemCollection;
use Sulu\Bundle\AdminBundle\Admin\View\ToolbarAction;
use Sulu\Bundle\AdminBundle\Admin\View\ViewBuilderFactoryInterface;
use Sulu\Bundle\AdminBundle\Admin\View\ViewCollection;
use Sulu\Component\Security\Authorization\PermissionTypes;
use Sulu\Component\Security\Authorization\SecurityCheckerInterface;
class ReviewAdmin extends Admin
{
const REVIEW_LIST_VIEW = 'review.list';
const REVIEW_EDIT_VIEW = 'review.edit_form';
const SECURITY_CONTEXT = 'sulu.review.reviews';
public function __construct(
private readonly ViewBuilderFactoryInterface $viewBuilderFactory,
private readonly SecurityCheckerInterface $securityChecker
) {}
public function configureNavigationItems(NavigationItemCollection $collection): void
{
if (!$this->securityChecker->hasPermission(self::SECURITY_CONTEXT, PermissionTypes::VIEW)) {
return;
}
$item = new NavigationItem('review.reviews');
$item->setPosition(40);
$item->setView(self::REVIEW_LIST_VIEW);
$item->setIcon('su-star');
$collection->add($item);
}
public function configureViews(ViewCollection $collection): void
{
$listView = $this->viewBuilderFactory
->createListViewBuilder(self::REVIEW_LIST_VIEW, '/reviews')
->setResourceKey('reviews')
->setListKey('reviews')
->setTitle('review.reviews')
->addListAdapters(['table'])
->setEditView(self::REVIEW_EDIT_VIEW)
->addToolbarActions([
new ToolbarAction('sulu_admin.add'),
new ToolbarAction('sulu_admin.delete'),
]);
$editView = $this->viewBuilderFactory
->createResourceTabViewBuilder(self::REVIEW_EDIT_VIEW, '/reviews/:id')
->setResourceKey('reviews')
->setBackView(self::REVIEW_LIST_VIEW);
$collection->add($listView);
$collection->add($editView);
}
public function getSecurityContexts(): array
{
return [
self::SECURITY_CONTEXT => [
PermissionTypes::VIEW,
PermissionTypes::ADD,
PermissionTypes::EDIT,
PermissionTypes::DELETE,
],
];
}
} REST API
// Controller/Admin/ReviewController.php
namespace App\ReviewBundle\Controller\Admin;
use App\ReviewBundle\Repository\ReviewRepository;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\ViewHandlerInterface;
use Sulu\Component\Rest\ListBuilder\Doctrine\DoctrineListBuilderFactoryInterface;
use Sulu\Component\Rest\ListBuilder\Metadata\FieldDescriptorFactoryInterface;
use Sulu\Component\Rest\RestHelperInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ReviewController extends AbstractFOSRestController
{
public function __construct(
ViewHandlerInterface $viewHandler,
private readonly ReviewRepository $repository,
private readonly RestHelperInterface $restHelper,
private readonly FieldDescriptorFactoryInterface $fieldDescriptorFactory,
private readonly DoctrineListBuilderFactoryInterface $listBuilderFactory
) {
parent::__construct($viewHandler);
}
#[Route('/api/reviews', methods: ['GET'])]
public function cgetAction(Request $request): Response
{
$fieldDescriptors = $this->fieldDescriptorFactory->getFieldDescriptors('reviews');
$listBuilder = $this->listBuilderFactory->create(Review::class);
$this->restHelper->initializeListBuilder($listBuilder, $fieldDescriptors);
$list = new ListRepresentation(
$listBuilder->execute(),
'reviews',
'review_api_review_cget',
$request->query->all(),
$listBuilder->getCurrentPage(),
$listBuilder->getLimit(),
$listBuilder->count()
);
return $this->handleView($this->view($list));
}
#[Route('/api/reviews', methods: ['POST'])]
public function postAction(Request $request): Response
{
$data = $request->toArray();
$review = $this->repository->createFromArray($data);
$this->repository->save($review, true);
return $this->handleView($this->view($review, 201));
}
#[Route('/api/reviews/{id}', methods: ['DELETE'])]
public function deleteAction(int $id): Response
{
$this->repository->removeById($id);
return $this->handleView($this->view(null, 204));
}
} מה כלול בעבודה?
- ניתוח דרישות מלא ומסמך המתאר ישויות, API ויחסים.
- יצירת מבנה ה-Bundle, רישום מחלקות Admin, סוגי מאפיינים, REST API.
- מיגרציות Doctrine ותצורת Symfony (services.xml, routes).
- תצוגות backoffice מותאמות אישית (React/Preact) במידת הצורך.
- בדיקות מקיפות: בדיקות יחידה, בדיקות פונקציונליות, בדיקת אינטגרציה עם Sulu.
- תיעוד: README, מדריך שדרוג, תיאור תצורה וסשן הדרכה.
- גישה למאגר פרטי לניהול גרסאות.
- שבועיים של תמיכת אחריות לאחר המסירה.
תהליך פיתוח Sulu Bundle ולוחות זמנים
- ניתוח ואב-טיפוס — הגדרת ישויות, שדות, יחסים, נקודות קצה של API. יצירת שלד Bundle. לוח זמנים: 1–2 ימים.
- עיצוב — עיצוב מחלקות Admin, ContentType, מיגרציות. תיאום חוויית המשתמש של ה-backoffice איתך. לוח זמנים: יום אחד.
- פיתוח — יישום כל הרכיבים, כתיבת קוד ובדיקות. לוח זמנים: 3–10 ימים.
- אינטגרציה — התקנה ותצורה בפרויקט שלך. לוח זמנים: יום אחד.
- תיעוד — README, מדריך שדרוג. לוח זמנים: 0.5 יום.
| שלב | תוצאה | לוח זמנים משוער |
|---|---|---|
| ניתוח ואב-טיפוס | מסמך עם ישויות ו-API | 1–2 ימים |
| עיצוב | סכימת Bundle, תבניות תצוגת Admin | יום אחד |
| פיתוח | קוד Bundle מלא עם בדיקות | 3–10 ימים |
| אינטגרציה | התקנה ותצורה בפרויקט שלך | יום אחד |
| תיעוד | README, מדריך שדרוג | 0.5 יום |
| תמיכת אחריות | שבועיים לאחר המסירה | — |
אילו טעויות נעשות לרוב בפיתוח Bundle?
- שכחת לרשום את השירות ב-services.xml — ללא התג
src/ └── ReviewBundle/ ├── Admin/ │ └── ReviewAdmin.php ├── Controller/ │ ├── Admin/ │ │ └── ReviewController.php │ └── Website/ │ └── ReviewWidgetController.php ├── DependencyInjection/ │ ├── ReviewExtension.php │ └── Configuration.php ├── Document/ ├── Entity/ │ └── Review.php ├── Repository/ │ └── ReviewRepository.php ├── Resources/ │ ├── config/ │ │ ├── doctrine/ │ │ │ └── Review.orm.xml │ │ ├── routes_admin.yaml │ │ └── services.xml │ └── js/ │ ├── index.js │ ├── views/ │ └── containers/ ├── ReviewBundle.php └── composer.json, מחלקת ה-Admin לא תופיע בניווט. - מרחב שמות שגוי — Symfony לא תמצא את המחלקה אם מרחב השמות אינו תואם לנתיב.
- לא הפעלת מיגרציות Doctrine — ישויות לא ייווצרו במסד הנתונים. הוסף את ה-Bundle ל-
// DependencyInjection/ReviewExtension.php namespace App\ReviewBundle\DependencyInjection; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; class ReviewExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); $container->setParameter('review.per_page', $config['per_page']); $container->setParameter('review.moderation', $config['moderation']); $loader = new XmlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') ); $loader->load('services.xml'); } }. - לא הגדרת זכויות גישה — משתמשים לא יראו את הסקציה. יישם
// Admin/ReviewAdmin.php namespace App\ReviewBundle\Admin; use Sulu\Bundle\AdminBundle\Admin\Admin; use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItem; use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItemCollection; use Sulu\Bundle\AdminBundle\Admin\View\ToolbarAction; use Sulu\Bundle\AdminBundle\Admin\View\ViewBuilderFactoryInterface; use Sulu\Bundle\AdminBundle\Admin\View\ViewCollection; use Sulu\Component\Security\Authorization\PermissionTypes; use Sulu\Component\Security\Authorization\SecurityCheckerInterface; class ReviewAdmin extends Admin { const REVIEW_LIST_VIEW = 'review.list'; const REVIEW_EDIT_VIEW = 'review.edit_form'; const SECURITY_CONTEXT = 'sulu.review.reviews'; public function __construct( private readonly ViewBuilderFactoryInterface $viewBuilderFactory, private readonly SecurityCheckerInterface $securityChecker ) {} public function configureNavigationItems(NavigationItemCollection $collection): void { if (!$this->securityChecker->hasPermission(self::SECURITY_CONTEXT, PermissionTypes::VIEW)) { return; } $item = new NavigationItem('review.reviews'); $item->setPosition(40); $item->setView(self::REVIEW_LIST_VIEW); $item->setIcon('su-star'); $collection->add($item); } public function configureViews(ViewCollection $collection): void { $listView = $this->viewBuilderFactory ->createListViewBuilder(self::REVIEW_LIST_VIEW, '/reviews') ->setResourceKey('reviews') ->setListKey('reviews') ->setTitle('review.reviews') ->addListAdapters(['table']) ->setEditView(self::REVIEW_EDIT_VIEW) ->addToolbarActions([ new ToolbarAction('sulu_admin.add'), new ToolbarAction('sulu_admin.delete'), ]); $editView = $this->viewBuilderFactory ->createResourceTabViewBuilder(self::REVIEW_EDIT_VIEW, '/reviews/:id') ->setResourceKey('reviews') ->setBackView(self::REVIEW_LIST_VIEW); $collection->add($listView); $collection->add($editView); } public function getSecurityContexts(): array { return [ self::SECURITY_CONTEXT => [ PermissionTypes::VIEW, PermissionTypes::ADD, PermissionTypes::EDIT, PermissionTypes::DELETE, ], ]; } }כמו בדוגמה לעיל.
לוחות זמנים ועלות
Bundle בסיסי עם ישות Doctrine, REST API ורישום backoffice (ללא פרונטאנד מותאם) — 5–7 ימים. עם פרונטאנד backoffice מותאם (רכיבי React/Preact), מיגרציות וסוגי מאפיינים מותאמים — 2–3 שבועות. שירותי פיתוח ה-sulu bundle שלנו מתחילים מ-$2,500 ויכולים לחסוך עד $20,000 בשנה בעלויות תחזוקה. פרויקט פיתוח Bundle טיפוסי עולה בין $2,500 ל-$10,000, ומניב חיסכון שנתי של עד $20,000. שימוש ב-Bundle מפחית את זמן תיקון הבאגים ב-70%. אנו נותנים הערכה מדויקת לאחר ניתוח המשימה שלך. צור קשר כדי להתחיל.
למה לסמוך עלינו?
אנו מפתחים Custom Sulu Bundles כבר למעלה מ-5 שנים, עם 30+ פרויקטים מוצלחים (98% שביעות רצון לקוחות). אנו מבטיחים אינטגרציה יציבה ועמידה בתקני הקידוד של Symfony. קבל ייעוץ לפרויקט שלך — פנה אלינו.







