Ilk modulunuz
title: İlk modülünüz (öğretici) description: Sıfırdan "Notlar" modülü: iskelet, tablo, varlık, yönetim ekranı, menü, yetki ve çeviri; yaklaşık yarım saat.
Bu öğreticide Notlar adlı küçük bir modül yazacağız: Studio'da bir menü öğesi, notları listeleyen ve yeni not ekleyen bir ekran, kendi veritabanı tablosu, yetkileri ve Türkçe/İngilizce çevirileriyle. Buradaki kodun tamamı gerçek bir CPalius kurulumunda çalıştırılıp doğrulanmıştır.
Bu öğretici 1.0 sürümünü (tek tablo, liste ve form) anlatır. Aynı modülün gerçek bir not defterine dönüşmüş 1.1 hâli için Örnek modül: Notlar 1.1 sayfasına bakın.
Ön koşul: çalışan bir CPalius kurulumu (bkz. Kurulum), PHP, Symfony ve Doctrine'e temel aşinalık.
Sonunda elde edeceğiniz:
cp-content/modules/Notes/
module.json
NotesModule.php
Install/ModuleInstaller.php
Entity/Note.php
Controller/Admin/NoteAdminController.php
Resources/
config/ capabilities.yaml routes.yaml services.yaml
migrations/ 001_notes.sql
translations/ messages+intl-icu.tr.yaml messages+intl-icu.en.yaml
views/admin/ index.html.twig
Adım 1: İskeleti üretin
php cp-core/bin/console cp:module:create Notes
Komut cp-content/modules/Notes/ altında manifest, bundle sınıfı, kurulumcu, çeviri dosyaları ve boş yapılandırma dosyalarını üretir. Ad PascalCase olmalıdır (Notes, RezervasyonSistemi).
Üretilen module.json:
{
"name": "Notes",
"version": "1.0.0",
"description": "Notes module.",
"author": "CPalius",
"bundle": "Modules\\Notes\\NotesModule",
"core_version": "^2.0",
"requires": {},
"conflicts": {},
"provides": ["notes"]
}
Bu alan, modülün hangi çekirdek sürümlerinde çalıştığını bildirir ve etkinleştirmede doğrulanır: uymuyorsa modül açılmaz ("Notes" requires CPalius core ^1.0, but 2.2.52 is installed). Eski çekirdeklerde cp:module:create sabit ^1.0 yazıyordu; böyle bir manifest alırsanız ^2.0 (ya da >=2.2.52 <3 gibi daha dar bir aralık) yapın.
Açıklamayı Türkçe yapmak isterseniz description'ı düzenleyin; mağazada bu görünür.
Adım 2: Tabloyu tanımlayın
Resources/migrations/001_notes.sql:
/* Notes module: one table. Idempotent on purpose. */
CREATE TABLE IF NOT EXISTS cp_notes (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(200) NOT NULL,
body LONGTEXT DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
Kurallar: dosyalar ada göre sırayla çalışır, ada göre bir kez kaydedilir (içeriğe bakılmaz), bu yüzden IF NOT EXISTS kullanın ve yayınlanmış bir dosyayı asla değiştirmeyin; yeni bir numaralı dosya (002_…sql) ekleyin. Ayrıntı: Veritabanı ve migration.
Kurulumcu hangi tabloları kaldırırken sileceğini bilmelidir. Install/ModuleInstaller.php:
final class ModuleInstaller extends AbstractSqlModuleInstaller
{
protected function moduleId(): string { return 'notes'; }
/** @return list<string> */
protected function tables(): array { return ['cp_notes']; }
}
Adım 3: Varlığı yazın
Entity/Note.php: Doctrine varlığı, tablonuza eşlenir:
<?php
declare(strict_types=1);
namespace Modules\Notes\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'cp_notes')]
class Note
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(type: Types::STRING, length: 200)]
public string $title = '';
#[ORM\Column(type: Types::TEXT, nullable: true)]
public ?string $body = null;
#[ORM\Column(name: 'created_at', type: Types::DATETIME_IMMUTABLE)]
public \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?int { return $this->id; }
}
Modülün Entity/ klasörü Doctrine'e otomatik tanıtılır; ayrıca eşleme yapılandırması yazmazsınız.
Adım 4: Yetkileri tanımlayın
Resources/config/capabilities.yaml:
capabilities:
- notes.view
- notes.manage
Bu iki yetenek kayıt defterine girer. admin rolü * jokerine sahip olduğundan ikisini de otomatik alır; başka roller için yetkileri AACP → Kullanıcılar'dan atarsınız. Ayrıntı: Yetki sistemi.
Adım 5: Rotaları yükleyin
Resources/config/routes.yaml:
notes_module_admin:
resource: ../../Controller/Admin/
type: attribute
Yönetim denetleyicileri /admin/... adresini kendileri taşır (önek eklenmez). Ön yüz denetleyicileriniz olsaydı bir /{_locale} önekiyle ayrı bir blok eklerdiniz (Belgeler modülünün routes.yaml dosyası örnektir).
Resources/config/services.yaml iskeletle hazır gelir ve Controller/ klasörünü otomatik tanımlar.
Adım 6: Denetleyici, menü ve yetki
Controller/Admin/NoteAdminController.php:
<?php
declare(strict_types=1);
namespace Modules\Notes\Controller\Admin;
use App\Core\Annotation\CpAdminMenu;
use Doctrine\ORM\EntityManagerInterface;
use Modules\Notes\Entity\Note;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/admin/notes', name: 'admin_notes_')]
final class NoteAdminController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $em)
{
}
#[Route('', name: 'index', methods: ['GET'])]
#[CpAdminMenu(label: 'notes.menu', icon: 'heroicons:pencil-square', panel: 'studio', priority: 50, capability: 'notes.view', group: 'studio.group.content')]
#[IsGranted('notes.view')]
public function index(): Response
{
return $this->render('@NotesModule/admin/index.html.twig', [
'notes' => $this->em->getRepository(Note::class)->findBy([], ['id' => 'DESC']),
'canManage' => $this->isGranted('notes.manage'),
]);
}
#[Route('/create', name: 'create', methods: ['POST'])]
#[IsGranted('notes.manage')]
public function create(Request $request): Response
{
if (!$this->isCsrfTokenValid('admin_notes', (string) $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$title = trim($request->request->getString('title'));
if ($title !== '') {
$note = new Note();
$note->title = mb_substr($title, 0, 200);
$note->body = trim($request->request->getString('body')) ?: null;
$this->em->persist($note);
$this->em->flush();
}
return $this->redirectToRoute('admin_notes_index');
}
}
Dikkat edilecek noktalar:
#[CpAdminMenu]menü öğesini kaydeder:panel: 'studio',group: 'studio.group.content'ile Studio'nun İçerik grubuna girer.labelbir çeviri anahtarıdır.capabilitykullanıcı o yeteneğe sahip değilse menüde hiç görünmez.#[IsGranted]ise rotayı korur. İkisi birbirinin yerine geçmez: menüyü gizlemek erişimi engellemez.- Yazma eylemi (
POST)notes.manageister ve CSRF belirtecini kendisi doğrular. - Girdi sınırlandırıldı (
mb_substr) ve kırpıldı.
Adım 7: Şablon
Resources/views/admin/index.html.twig. Bundle adından (NotesModule) türeyen @NotesModule ad alanı otomatik oluşur:
{% extends 'admin/layout.html.twig' %}
{% block header %}{{ 'notes.menu'|trans }}{% endblock %}
{% block body %}
{% if canManage %}
<twig:studio:card>
<form method="post" action="{{ path('admin_notes_create') }}" class="grid gap-3">
<input type="hidden" name="_token" value="{{ csrf_token('admin_notes') }}">
<div>
<label class="form-label" for="title">{{ 'notes.title'|trans }}</label>
<input class="form-control" id="title" name="title" maxlength="200" required>
</div>
<div>
<label class="form-label" for="body">{{ 'notes.body'|trans }}</label>
<textarea class="form-control" id="body" name="body" rows="3"></textarea>
</div>
<button class="btn btn-primary self-start">{{ 'notes.add'|trans }}</button>
</form>
</twig:studio:card>
{% endif %}
<twig:studio:card class="mt-sp-md">
{% for note in notes %}
<article class="border-b border-neutral-200 py-2 last:border-0">
<h3 class="font-semibold">{{ note.title }}</h3>
{% if note.body %}<p class="text-fs-sm text-neutral-500">{{ note.body }}</p>{% endif %}
</article>
{% else %}
<p class="text-neutral-500">{{ 'notes.empty'|trans }}</p>
{% endfor %}
</twig:studio:card>
{% endblock %}
admin/layout.html.twig Studio'nun ortak şablonudur; <twig:studio:card> Studio'nun kart bileşenidir. {{ note.title }} Twig'de otomatik kaçışlanır, kullanıcı girdisini ham basmayın.
Adım 8: Çeviriler
Resources/translations/messages+intl-icu.tr.yaml:
notes.menu: "Notlar"
notes.title: "Başlık"
notes.body: "Not"
notes.add: "Not ekle"
notes.empty: "Henüz not yok."
messages+intl-icu.en.yaml:
notes.menu: "Notes"
notes.title: "Title"
notes.body: "Note"
notes.add: "Add note"
notes.empty: "No notes yet."
Her anahtarı iki dilde de yazın; cp:doctor eşitliği denetler. Modül, TR ve EN dosyası olmadan paketlenemez (paket denetimi İngilizce kataloğu şart koşar).
Adım 9: Etkinleştirin
php cp-core/bin/console cp:module:activate Notes
Etkinleştirme sırasıyla şunları yapar: bağımlılık denetimi (core_version, requires), yalıtılmış alt süreçte derleme ve YAML denetimi, active_modules.php'ye ekleme, kurulumcunun çalışması ve 001_notes.sql'in uygulanması. Bir şey ters giderse modül etkinleşmez ve sitenize dokunulmaz.
Sonra Studio'yu açın: İçerik grubunda Notlar görünür; bir not ekleyin.
Adım 10: Hata ayıklama
| Belirti | Bakın |
|---|---|
| Etkinleşmiyor | Hata mesajı dosya ve satırı söyler; core_version, YAML girintisi, bundle sınıfı adı |
| Menüde yok | Kullanıcının notes.view yeteneği var mı; önbellek temiz mi (cache:clear) |
| 403 | #[IsGranted] adı capabilities.yaml ile birebir aynı mı |
| Tablo yok | cp:doctor modül SQL tutarlılığını söyler; migration dosya adı 00N_ ile başlıyor mu |
| Şablon bulunamadı | Ad alanı @<Ad>Module (bundle adı): @NotesModule/... |
| Sayfa boş / kırık | cp-core/var/log/module_quarantine.log |
php cp-core/bin/console cp:debug sistemin modülünüzde ne bulduğunu (yetenekler, hook'lar, cron işleri, varlık türleri) listeler.
Sırada ne var?
- Notlara alanlar ve çok dillilik eklemek istiyorsanız
Nodetabanlı içerik türü kullanın: Alan sistemi. - İş kaydı olacaksa
#[CpResource]ile otomatik yönetim ekranı ve yetkiler: İçerik modeli. - Ön yüzde göstermek için bir
/{_locale}/notesrotası ekleyin: Modül iskeleti. - Menü, ayar ve arama: Menü ve yetkiler, Arama, ayar ve SEO.
- Yayınlamak için: Mağazaya yayınlama ve Kontrol listesi.