Chapterبەش 8 · Lectureوانە 8.10

ORM: Working with Tables as ObjectsORM: کارکردن لەگەڵ خشتەکان وەک ئۆبجێکتەکان

Why rows and objects do not fit together, what an ORM does (classes and tables, objects and rows, relations), the Active Record and Data Mapper patterns, a small hand-made mapper in PHP with PDO and SQLite that you can run, Eloquent and Doctrine as they look in 2026, safe queries, the N+1 query problem measured, migrations in one screen, and when plain SQL is the better choice.

بۆچی و ڕێک لەگەڵ یەک ناکەون، چی دەکات ( و ، و ، )، هەردوو شێوازی و ، هاوتاکەرێکی بچووکی دەستکرد بە PHP، PDO و SQLite کە خۆت کاری پێ دەکەیت، Eloquent و Doctrine وەک لە ⁦2026⁩ـدا هەن، سەلامەت، بە پێوانەوە، لە یەک شاشەدا و ئەو کاتانەی سادە باشترە.

22 min read19 خولەک خوێندنەوە · 18 stepsهەنگاو

What you will learnچی فێر دەبیت

  • Explain why rows and objects do not fit together: identity, links, types and inheritance.
  • Describe what an ORM maps and what it does for you.
  • Tell Active Record from Data Mapper, and read Eloquent and Doctrine code.
  • Build and use a small mapper with PDO and SQLite, with bound parameters and an allow-list for names.
  • Spot the N+1 query problem, fix it with eager loading, and know when plain SQL or a migration is the right tool.
  • ڕوونی بکەیتەوە بۆچی و ڕێک ناکەون: ناسنامە، بەستنەوە، جۆر و .
  • وەسفی ئەوە بکەیت کە چی هاوتا دەکات و چیت بۆ دەکات.
  • لە جیا بکەیتەوە و کۆدی Eloquent و Doctrine بخوێنیتەوە.
  • هاوتاکەرێکی بچووک بە PDO و دروست بکەیت و بەکاری بهێنیت، بە بەستراو و بۆ ناوەکان.
  • بدۆزیتەوە، بە چارەسەری بکەیت و بزانیت کەی سادە یان ئامرازی دروستە.

Rows in the database, objects in PHPتۆمارەکان لە بنکەدراوەدا، ئۆبجێکتەکان لە PHPـدا

In the CRUD application every page did the same work by hand. To show a student it ran a SELECT, received the row as an array and printed $row['name']. To save a student it listed every column again in an INSERT or an UPDATE. Meanwhile, Classes and Objects showed a better home for data: an object such as $aram, made from a class Student, with properties and methods.

لە بەرنامەی CRUDـدا هەموو هەمان کاری بە دەست دەکرد. بۆ پیشاندانی قوتابییەک SELECTـێکی دەنارد، وەک وەردەگرت و $row['name']ـی چاپ دەکرد. بۆ هەڵگرتنی قوتابییەک، دیسان هەموو لە INSERT یان UPDATEـدا دەنووسییەوە. لە هەمان کاتدا کلاسەکان و ئۆبجێکتەکان شوێنێکی باشتری بۆ پیشان دا: وەک $aram کە لە Student دروست کراوە، بە و .

So in every page that works with a database two worlds meet: the database keeps rows, and your PHP code wants objects. Something must copy between them, every time and in both directions. An ORM (object–relational mapping) is a library that does this copying for you.

کەواتە لە هەموو ئەو کار لەگەڵ دەکەن، دوو جیهان بەیەک دەگەن: هەڵدەگرێت و کۆدی PHPـەکەت دەوێت. شتێک دەبێت هەموو جارێک و بە هەردوو ئاراستەدا لە نێوانیاندا کۆپی بکات. (هاوتاکردنی و پەیوەندی) کە ئەم کۆپیکردنەت بۆ دەکات.

Left: a table called students with the columns id, name and marks and two rows: 1, Aram, 85 and 2, Shilan, 92. Right: two objects of class Student, $aram and $shilan, holding the same values as properties. An arrow labelled load goes from the table to the objects, and an arrow labelled save goes back. Below: without an ORM you write this copying by hand on every page: SELECT, then fill an object; read an object, then INSERT or UPDATE.لای چەپ: خشتەیەک بە ناوی students بە ستوونەکانی id، name و marks و دوو تۆمار: 1، Aram، 85 و 2، Shilan، 92. لای ڕاست: دوو ئۆبجێکتی کلاسی Student، واتە $aram و $shilan، کە هەمان نرخەکان وەک تایبەتمەندی هەڵدەگرن. تیرێک بە ناوی هێنان لە خشتەکەوە دەچێت بۆ ئۆبجێکتەکان و تیرێک بە ناوی هەڵگرتن دەگەڕێتەوە. لە خوارەوە: بەبێ ORM ئەم کۆپیکردنە لە هەموو لاپەڕەتۆڕێکدا بە دەست دەنووسیت: SELECT، پاشان پڕکردنەوەی ئۆبجێکتێک؛ خوێندنەوەی ئۆبجێکتێک، پاشان INSERT یان UPDATE.
One student, two shapes: a row in the table and an object in PHP.یەک قوتابی، دوو شێوە: لە و لە PHPـدا.CêIT

Why rows and objects do not fitبۆچی تۆمار و ئۆبجێکت ڕێک ناکەون

Tables and objects were made for different jobs. Tables keep facts so that many programs can search them; objects keep data and behaviour together inside one program. Programmers call the friction between the two the object–relational impedance mismatch. Four differences cause most of it:

و بۆ دوو کاری جیاواز دروست کراون. ڕاستییەکان هەڵدەگرن بۆ ئەوەی زۆر بتوانن تێیاندا بگەڕێن؛ و ڕەفتار لە ناو یەک پێکەوە کۆ دەکەنەوە. بەرنامەسازان بەم ناکۆکییەی نێوانیان دەڵێن ⁦object–relational impedance mismatch⁩. چوار جیاوازی زۆربەی کێشەکە دروست دەکەن:

  • Who is it? A row is known by its primary key (id = 1). In PHP, two SELECTs of row 1 can give two different objects that both say id = 1.
  • Links. A table points to another table with a foreign key (department_id = 2); an object simply holds the other object ($aram->department).
  • Types. SQLite stores only NULL, INTEGER, REAL, TEXT and BLOB: true and false become 1 and 0, and dates become text or numbers. PHP has int, float, string, bool, null, arrays and objects such as DateTime.
  • Families. class Teacher extends Person (inheritance) has no twin in a table, so the ORM must choose how to store it.
  • کێیە؟ بە دەناسرێتەوە (id = 1). لە PHPـدا دوو SELECTـی 1 دەتوانن دوو جیاواز بدەن کە هەردووکیان دەڵێن id = 1.
  • بەستنەوە. بە ئاماژە بە تر دەکات (department_id = 2)؛ بە سادەیی ترەکە لە خۆیدا هەڵدەگرێت ($aram->department).
  • جۆرەکان. تەنیا NULL، INTEGER، REAL، TEXT و BLOB هەڵدەگرێت: true و false دەبن بە 1 و 0 و ڕێکەوتەکان دەبن بە دەق یان ژمارە. PHP int، float، string، bool، null، و وەک DateTimeـی هەیە.
  • خێزان. class Teacher extends Person (بۆماوە) هیچ هاوتایەکی لە نییە، بۆیە دەبێت بڕیار بدات چۆن هەڵیبگرێت.
Two columns, the table world and the object world, compared in four rows. Who is it: the primary key id = 1, or the object itself, $aram. Links: a foreign key department_id = 2, or an object inside, $aram->department. Types: INTEGER, TEXT, NULL and no true or false, or int, string, bool, null and objects. Families: no inheritance, only tables, or class Teacher extends Person. Below: an ORM translates between the two worlds in both directions.دوو ستوون، جیهانی خشتەکان و جیهانی ئۆبجێکتەکان، لە چوار ڕیزدا بەراورد کراون. کێیە: کلیلی سەرەکی id = 1، یان خودی ئۆبجێکتەکە، $aram. بەستنەوە: کلیلی بیانی department_id = 2، یان ئۆبجێکتێک لە ناوەوە، $aram->department. جۆرەکان: INTEGER، TEXT، NULL و هیچ true یان false نییە، یان int، string، bool، null و ئۆبجێکتەکان. خێزان: بۆماوە نییە و تەنیا خشتەکان هەن، یان class Teacher extends Person. لە خوارەوە: ORM لە نێوان هەردوو جیهانەکەدا بە هەردوو ئاراستەدا وەردەگێڕێت.
Four differences between the table world and the object world.چوار جیاوازی لە نێوان جیهانی و جیهانی .CêIT

What an ORM doesORM چی دەکات

Definitionپێناسە

Object–relational mapping: a that connects the and of a program with the and of a . It writes the for you, so your code loads and saves objects instead of rows.

هاوتاکردنی ئۆبجێکت و پەیوەندی: کە و دەبەستێتەوە بە و . بۆت دەنووسێت، بۆیە کۆدەکەت لەبری دەهێنێت و هەڵدەگرێت.

An ORM keeps a map between the two worlds: each class goes with a table, each object with a row, each property with a column and each object link with a foreign key. With that map it can write the SQL (SELECT, INSERT, UPDATE, DELETE) for you, convert types in both directions, load linked objects when you ask for them, and notice which objects changed so that it saves only those. In a program organised in layers, the ORM belongs to the data access layer: see Layers and Business Logic.

نەخشەیەک لە نێوان هەردوو جیهانەکەدا دەپارێزێت: هەر لەگەڵ ، هەر لەگەڵ ، هەر لەگەڵ و هەر بەستنەوەیەکی لەگەڵ . بەو نەخشەیە دەتوانێت (SELECT، INSERT، UPDATE، DELETE) بۆت بنووسێت، جۆرەکان بە هەردوو ئاراستەدا بگۆڕێت، ئەو بەستراون کاتێک داوایان دەکەیت بهێنێت و بزانێت کام گۆڕاون، بۆ ئەوەی تەنیا ئەوانە هەڵبگرێت. لە کە بە ڕێکخراوە، بەشێکە لە : بڕوانە چینەکان و لۆجیکی کاروبار.

Everyday analogyنموونەیەک لە ژیانی ڕۆژانەوە

An ORM is like a translator in the bazaar of Erbil, standing between a Kurdish shopkeeper and a visitor who speaks only English. Each of them speaks normally, and the translator turns every sentence into the other language. Your code speaks objects, the database speaks rows and SQL, and the ORM translates.

وەک وەرگێڕێکە لە بازاڕی هەولێر، لە نێوان دوکاندارێکی کورد و میوانێک کە تەنیا ئینگلیزی دەزانێت. هەریەکەیان بە ئاسایی قسە دەکات و وەرگێڕەکە هەموو ڕستەیەک دەکاتە زمانەکەی تر. کۆدەکەت بە قسە دەکات، بە و و وەردەگێڕێت.

Where the analogy stops workingسنووری ئەم نموونەیە

A translator still needs you to understand the deal. When a sentence is long and difficult, such as a big report or a change to thousands of rows, it is often clearer to speak SQL yourself.

وەرگێڕ هێشتا پێویستی بەوەیە کە تۆ لە مامەڵەکە تێبگەیت. کاتێک ڕستەیەک درێژ و قورسە، وەک گەورە یان گۆڕینی هەزاران ، زۆرجار ڕوونترە خۆت بە قسە بکەیت.

The ORM stands in the middle between your PHP code on the left and the database on the right. Four pairs are joined by two-way arrows: class Student with the table students, the object $aram with one row, the property ->name with the column name, and $aram->department with the foreign key department_id. Inside the ORM: writes the SQL, converts types, tracks changes.ORM لە ناوەڕاستدایە، لە نێوان کۆدی PHPـی تۆ لە چەپ و بنکەدراوەکە لە ڕاست. چوار جووت بە تیری دوولایەنە بەستراون: class Student لەگەڵ خشتەی students، ئۆبجێکتی $aram لەگەڵ یەک تۆمار، تایبەتمەندیی name لەگەڵ ستوونی name و $aram->department لەگەڵ کلیلی بیانی department_id. لە ناو ORMـدا: SQL دەنووسێت، جۆرەکان دەگۆڕێت، گۆڕانکاری دەپارێزێت.
The ORM sits between your classes and your tables and keeps the map. لە نێوان و دادەنیشێت و نەخشەکە دەپارێزێت.CêIT

A small mapper you can runهاوتاکەرێکی بچووک کە دەتوانیت کاری پێ بکەیت

The best way to see what an ORM does for you is to build a tiny one. It needs nothing new: PDO with SQLite, as in Using SQLite from PHP. Student is a plain class whose constructor uses property promotion. StudentMapper moves students between objects and the students table, so it is a small Data Mapper. Every query uses ? placeholders, so the values are always bound (prepared statements with PDO).

باشترین ڕێگا بۆ بینینی ئەوەی چیت بۆ دەکات ئەوەیە کە یەکێکی بچووک دروست بکەیت. هیچ شتێکی نوێی ناوێت: PDO لەگەڵ ، وەک بەکارهێنانی SQLite لە PHPـەوە. Student سادەیە و بەرزکردنەوەی پارامیتەر بۆ تایبەتمەندی بەکار دەهێنێت. StudentMapper قوتابییەکان لە نێوان و studentsـدا دەگوازێتەوە، بۆیە نموونەیەکی بچووکی . هەموو شوێنی ? بەکار دەهێنێت، بۆیە نرخەکان هەمیشە بە جیا دەنێردرێن (ڕستە ئامادەکراوەکان بە PDO).

A hand-made Data Mapperهاوتاکەرێکی دراوە بە دەستmapper.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// mapper.php - a tiny hand-made ORM (a Data Mapper): PDO + SQLite, nothing to install

class Student
{
    public ?int $id = null;               // null = not in the database yet

    public function __construct(
        public string $name,
        public int $marks,
    ) {}
}

class StudentMapper
{
    public function __construct(private PDO $db) {}

    // One row (an array) becomes one object
    private function toObject(array $row): Student
    {
        $student = new Student($row['name'], (int) $row['marks']);
        $student->id = (int) $row['id'];
        return $student;
    }

    // Every query goes through here, so we can see the SQL the mapper writes
    private function run(string $sql, array $params = []): PDOStatement
    {
        echo "  SQL> $sql  ", json_encode($params), "\n";
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    public function find(int $id): ?Student
    {
        $row = $this->run('SELECT id, name, marks FROM students WHERE id = ?', [$id])
                    ->fetch(PDO::FETCH_ASSOC);
        return $row ? $this->toObject($row) : null;
    }

    /** @return Student[] */
    public function findAll(): array
    {
        $rows = $this->run('SELECT id, name, marks FROM students ORDER BY id')
                     ->fetchAll(PDO::FETCH_ASSOC);
        return array_map(fn(array $row) => $this->toObject($row), $rows);
    }

    // One object becomes one row: INSERT a new object, UPDATE an old one
    public function save(Student $student): void
    {
        if ($student->id === null) {
            $this->run('INSERT INTO students (name, marks) VALUES (?, ?)',
                       [$student->name, $student->marks]);
            $student->id = (int) $this->db->lastInsertId();
        } else {
            $this->run('UPDATE students SET name = ?, marks = ? WHERE id = ?',
                       [$student->name, $student->marks, $student->id]);
        }
    }
}
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 6

?int with = null: a new object has no id until the database gives it one.

?int لەگەڵ = null: نوێ هیچ idـێکی نییە تا یەکێکی پێ دەدات.

Lineدێڕی 19-23

One row (an array) becomes one object: the heart of every ORM.

یەک () دەبێت بە یەک : دڵی هەموو .

Lineدێڕی 27-32

Every query passes through here: it prints the SQL, then prepares it and binds the values.

هەموو لێرەوە تێدەپەڕێت: چاپ دەکات، پاشان ئامادەی دەکات و نرخەکان دەبەستێتەوە.

Lineدێڕی 51-59

No id: INSERT, then read the new id. An id: UPDATE that row.

بێ id: INSERT، پاشان idـی نوێ دەخوێنێتەوە. بە id: UPDATEی ئەو .

A flowchart of $mapper->save($student). A diamond asks: $student->id === null? Yes, the object is new: INSERT INTO students, then $student->id = lastInsertId(); the database chooses the id. No, the object is old: UPDATE students SET ... WHERE id = ?; the id says which row. Below: after save(), the row and the object hold the same values.هێڵکارییەکی ڕەوتی $mapper->save($student). چوارگۆشەیەکی لار دەپرسێت: $student->id === null? بەڵێ، ئۆبجێکتەکە نوێیە: INSERT INTO students، پاشان $student->id = lastInsertId()؛ بنکەدراوەکە id هەڵدەبژێرێت. نەخێر، ئۆبجێکتەکە کۆنە: UPDATE students SET ... WHERE id = ?؛ id دەڵێت کام تۆمار. لە خوارەوە: دوای save()، تۆمارەکە و ئۆبجێکتەکە هەمان نرخیان هەیە.
save() asks one question: is this object new?⁦save()⁩ یەک پرسیار دەکات: ئایا ئەم نوێیە؟CêIT

A small mapper you can runهاوتاکەرێکی بچووک کە دەتوانیت کاری پێ بکەیت

Objects in, objects outئۆبجێکت دەچێتە ژوورەوە، ئۆبجێکت دێتە دەرەوە

Now use the mapper. use-mapper.php contains no SQL at all: it creates objects, changes one of them and asks the mapper to save them. The SQL> lines are printed by the mapper's run(), so you can watch the SQL that an ORM normally writes for you in silence.

ئێستا هاوتاکەرەکە بەکار بهێنە. use-mapper.php هیچ تێدا نییە: دروست دەکات، یەکێکیان دەگۆڕێت و داوا لە هاوتاکەرەکە دەکات هەڵیانبگرێت. دێڕەکانی SQL> لەلایەن run()ـی هاوتاکەرەکەوە چاپ دەکرێن، بۆیە دەتوانیت ئەو ببینیت کە بە ئاسایی بە بێدەنگی بۆت دەنووسێت.

Using the mapper: no SQL in this fileبەکارهێنانی هاوتاکەرەکە: هیچ SQLـێک لەم فایلەدا نییەuse-mapper.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// use-mapper.php - objects in, objects out: no SQL in this file
require __DIR__ . '/mapper.php';

$db = new PDO('sqlite::memory:');                  // a database in memory, for the demo
$db->exec('CREATE TABLE students (
    id    INTEGER PRIMARY KEY,
    name  TEXT    NOT NULL,
    marks INTEGER NOT NULL
)');
$mapper = new StudentMapper($db);

$aram = new Student('Aram', 85);
$mapper->save($aram);                              // new object: INSERT
$mapper->save(new Student('Shilan', 92));
echo "Aram's id is now {$aram->id}\n";

$aram->marks = 88;                                 // change the object...
$mapper->save($aram);                              // ...and save it: UPDATE

foreach ($mapper->findAll() as $s) {
    echo "{$s->id}: {$s->name} has {$s->marks} marks\n";
}
var_dump($mapper->find(9));                        // no such row: null
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 5

sqlite::memory: keeps the database in memory for the demo; a file path works the same way.

sqlite::memory: بۆ ئەم نموونەیە لە هەڵدەگرێت؛ ڕێڕەوی بە هەمان شێوە کار دەکات.

Lineدێڕی 18-19

Change the object, then save it: the mapper sends an UPDATE.

بگۆڕە، پاشان هەڵیبگرە: هاوتاکەرەکە UPDATEـێک دەنێرێت.

Read the output from the top: two INSERTs; then an UPDATE, because $aram already had id 1; then the SELECT of findAll(). find(9) gives NULL, not an error, because no row has id 9. A real ORM adds much more (relations, type conversion, change tracking), but this is its core.

لە سەرەوە بخوێنەوە: دوو INSERT؛ پاشان UPDATEـێک، چونکە $aram پێشتر ⁦id 1⁩ـی هەبوو؛ پاشان SELECTـی findAll(). find(9) NULL دەداتەوە نەک ، چونکە هیچ idـی 9ـی نییە. ڕاستەقینە زۆر شتی تر زیاد دەکات (، ، شوێنکەوتنی گۆڕانکاری)، بەڵام بیرۆکە سەرەکییەکەی ئەمەیە.

Two designs: Active Record and Data Mapperدوو دیزاین: تۆماری چالاک و هاوتاکەری دراوە

Martin Fowler gave the two common designs their names in his book Patterns of Enterprise Application Architecture (2002).

Martin Fowler لە کتێبی Patterns of Enterprise Application Architecture ⁦(2002)⁩ـدا ناوی ئەم دوو دیزاینە باوەی دانا.

  • Active Record: each object wraps one row and loads, saves and deletes itself: $student->save(). The class knows its table. It is quick to learn; Laravel's Eloquent and Ruby on Rails work this way.
  • Data Mapper: plain objects know nothing about the database, and a separate mapper moves the data both ways: $mapper->save($student). There are more pieces, but your business rules stay free of SQL and are easy to test. Doctrine works this way, and so does our StudentMapper.
  • : هەر یەک لە خۆ دەگرێت و خۆی دەهێنێت، هەڵدەگرێت و دەسڕێتەوە: $student->save(). دەناسێت. فێربوونی خێرایە؛ Eloquentـی Laravel و Ruby on Rails بەم شێوەیە کار دەکەن.
  • : سادەن و هیچ لە نازانن و هاوتاکەرێکی جیا بە هەردوو ئاراستەدا دەگوازێتەوە: $mapper->save($student). پارچەی زیاترە، بەڵام یاساکانی کارەکەت بێ دەمێننەوە و تاقیکردنەوەیان ئاسانە. Doctrine بەم شێوەیە کار دەکات، StudentMapperـەکەی ئێمەش هەروەها.
Two panels. Active Record: one object $student holds name and marks and also save(), find() and delete(); the SQL is inside the object, and an arrow goes straight to the database; the code is $student->save();. Data Mapper: a plain $student with name and marks and no SQL sits next to a separate $mapper with save($s) and find($id); the SQL is in the mapper, and only the mapper talks to the database; the code is $mapper->save($student);.دوو بەش. تۆماری چالاک: ئۆبجێکتێک بە ناوی $student کە name و marks و هەروەها save()، find() و delete() هەڵدەگرێت؛ SQL لە ناو ئۆبجێکتەکەدایە و تیرێک ڕاستەوخۆ دەچێت بۆ بنکەدراوە؛ کۆدەکە $student->save();ـە. هاوتاکەری دراوە: $studentـێکی سادە بە name و marks و بێ SQL، لە تەنیشت $mapperـێکی جیا بە save($s) و find($id)؛ SQL لە هاوتاکەرەکەدایە و تەنیا هاوتاکەرەکە قسە لەگەڵ بنکەدراوە دەکات؛ کۆدەکە $mapper->save($student);ـە.
Active Record: the object saves itself. Data Mapper: a mapper saves the object.: خۆی هەڵدەگرێت. : هاوتاکەرێک هەڵدەگرێت.CêIT

Eloquent (Laravel): Active Record in practice⁦Eloquent (Laravel)⁩: تۆماری چالاک لە کاردا

Laravel is a popular PHP framework, and Eloquent is its ORM. As of November 2026 the current version is Laravel 13, released on 17 March 2026. It needs PHP 8.3 or newer and is installed with Composer, so it does not run on the PHP 8.2 of this course's XAMPP. The code below is not run here: it follows the official Laravel 13 documentation.

Laravel بەناوبانگی PHPـە و Eloquent . لە تشرینی دووەمی ⁦2026⁩ـدا وەشانی ئێستا ⁦Laravel 13⁩ـە کە لە ⁦17⁩ی ئازاری ⁦2026⁩ بڵاو کرایەوە. ⁦PHP 8.3⁩ یان نوێتری پێویستە و بە Composer دادەمەزرێت، بۆیە لەسەر ⁦PHP 8.2⁩ـی XAMPPـی ئەم کۆرسە کار ناکات. ئەم کۆدەی خوارەوە لێرە جێبەجێ نەکراوە: بەپێی فەرمیی ⁦Laravel 13⁩ نووسراوە.

A model: one class for one table (not run)مۆدێلێک: یەک کلاس بۆ یەک خشتە (جێبەجێ نەکراوە)app/Models/Student.phpPHP
<?php
// app/Models/Student.php - Laravel 13 (not run in this lesson)
namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['name', 'marks'])]
class Student extends Model
{
    // table "students", primary key "id", created_at and updated_at: all by convention
}
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 8

Fillable lists the only columns that create() may fill. It stops mass assignment: a visitor who adds is_admin=1 to a form cannot set is_admin.

Fillable تەنیا ئەو لیست دەکات کە create() دەتوانێت پڕیان بکاتەوە. ڕێگە لە دەگرێت: سەردانکەرێک کە is_admin=1 بۆ زیاد دەکات ناتوانێت is_admin دابنێت.

Using the model (not run)بەکارهێنانی مۆدێلەکە (جێبەجێ نەکراوە)PHP
<?php
// Using the model in Laravel 13 (not run in this lesson)
use App\Models\Student;

$all  = Student::all();                                   // every student
$good = Student::where('marks', '>=', 80)->orderBy('name')->get();
$aram = Student::find(1);                                 // one object, or null

$aram->marks = 88;
$aram->save();                                            // UPDATE ... WHERE id = 1

$hana = Student::create(['name' => 'Hana', 'marks' => 90]);   // INSERT
The code class Student extends Model, with nothing inside, points with an arrow labelled Eloquent to a table students with the columns id, name, marks, created_at and updated_at. Below, the conventions: table name: the class name in snake_case and plural, Student to students, and AirTrafficController to air_traffic_controllers; primary key: a whole-number column named id; timestamps: created_at and updated_at are filled in for you; to change a rule, add an attribute to the class, for example #[Table('pupils')].کۆدی class Student extends Model بێ هیچ شتێک لە ناویدا، بە تیرێک بە ناوی Eloquent ئاماژە بە خشتەی students دەکات بە ستوونەکانی id، name، marks، created_at و updated_at. لە خوارەوە یاسا باوەکان: ناوی خشتە: ناوی کلاسەکە بە snake_case و کۆ، Student دەبێت بە students و AirTrafficController دەبێت بە air_traffic_controllers؛ کلیلی سەرەکی: ستوونێک بە ناوی id کە ژمارەی تەواوە؛ کاتەکان: created_at و updated_at بۆت پڕ دەکرێنەوە؛ بۆ گۆڕینی یاسایەک ئاتریبیوتێک بخەرە سەر کلاسەکە، بۆ نموونە #[Table('pupils')].
Eloquent guesses the table from the class name, unless you say otherwise.Eloquent لە ناوی دەدۆزێتەوە، مەگەر خۆت شتێکی تر بڵێیت.CêIT

Doctrine: Data Mapper in practiceDoctrine: هاوتاکەری دراوە لە کاردا

Doctrine ORM is the other big PHP ORM; the Symfony framework uses it for databases. As of November 2026 the maintained version is Doctrine ORM 3.7, and 4.0 is on the way. Its documentation says it is built on the Data Mapper pattern: your classes, called entities, are plain PHP; attributes such as #[ORM\Column] describe the table; and an EntityManager does the loading and saving. Not run here; it follows Doctrine's Getting Started tutorial.

Doctrine ORM ئەو گەورەکەی تری PHPـە و Symfony بۆ بەکاری دەهێنێت. لە تشرینی دووەمی ⁦2026⁩ـدا وەشانی پشتگیریکراو ⁦Doctrine ORM 3.7⁩ـە و ⁦4.0⁩ لە ڕێگادایە. دەڵێت لەسەر دروست کراوە: ، کە پێیان دەوترێت ، PHPـی سادەن؛ ئاتریبیوتەکانی وەک #[ORM\Column] وەسف دەکەن؛ EntityManagerـیش هێنان و هەڵگرتن دەکات. لێرە جێبەجێ نەکراوە؛ بەپێی وانەی Getting Startedـی Doctrine نووسراوە.

An entity: a plain class with attributes (not run)قەوارەیەک: کلاسێکی سادە بە ئاتریبیوتەوە (جێبەجێ نەکراوە)src/Student.phpPHP
<?php
// src/Student.php - Doctrine ORM 3 (not run in this lesson)
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'students')]
class Student
{
    #[ORM\Id]
    #[ORM\Column(type: 'integer')]
    #[ORM\GeneratedValue]
    private int|null $id = null;

    #[ORM\Column(type: 'string')]
    private string $name;

    #[ORM\Column(type: 'integer')]
    private int $marks;

    // getters and setters: getId(), getName(), setName(), setMarks() ...
}
persist(), flush() and find() (not run)⁦persist()⁩، ⁦flush()⁩ و ⁦find()⁩ (جێبەجێ نەکراوە)PHP
<?php
// Saving and finding with Doctrine (not run in this lesson)
$hana = new Student();
$hana->setName('Hana');
$hana->setMarks(90);

$entityManager->persist($hana);   // "remember to save this one": nothing is written yet
$entityManager->flush();          // now the INSERT runs, inside one transaction

$found = $entityManager->find(Student::class, 1);                    // one object, or null
$all   = $entityManager->getRepository(Student::class)->findAll();   // every student

Doctrine remembers every entity it has loaded or been asked to persist; this list is its Unit of Work. flush() works out what changed and writes everything in one transaction, so you do not save objects one by one.

Doctrine هەموو لە بیری دەمێنێت کە هێنابێتی یان داوای persistـی لێ کرابێت؛ ئەم لیستە Unit of Workـەکەیەتی. flush() دەزانێت چی گۆڕاوە و هەموو شتێک لە یەک دەنووسێت، بۆیە یەک یەک هەڵناگریت.

Three lines of code, $em->persist($hana);, $em->persist($rebin); and $soran->marks = 75;, all point to a box called Unit of Work: a list of objects to write later: Hana (new), Rebin (new), Soran (changed). Below it, $em->flush(); sends everything to a box called one transaction: BEGIN, INSERT ... ('Hana'), INSERT ... ('Rebin'), UPDATE ... marks = 75, COMMIT. Bottom: nothing reaches the database until flush(); then everything is written together.سێ دێڕ کۆد، $em->persist($hana);، $em->persist($rebin); و $soran->marks = 75;، هەموویان ئاماژە بە چوارگۆشەیەک دەکەن بە ناوی Unit of Work: لیستێکی ئۆبجێکتەکان بۆ نووسینی دواتر: Hana (نوێ)، Rebin (نوێ)، Soran (گۆڕاو). لە خوارەوەی، $em->flush(); هەموو شتێک دەنێرێت بۆ چوارگۆشەیەک بە ناوی یەک مامەڵە: BEGIN، INSERT ... ('Hana')، INSERT ... ('Rebin')، UPDATE ... marks = 75، COMMIT. لە خوارەوە: هیچ شتێک ناگاتە بنکەدراوە تا flush()؛ ئەوسا هەموو شتێک پێکەوە دەنووسرێت.
persist() only adds to a list; flush() writes everything in one transaction.⁦persist()⁩ تەنیا دەیخاتە سەر لیستێک؛ ⁦flush()⁩ هەموو شتێک لە یەک دەنووسێت.CêIT

Safe queries: what the ORM protects, and what it does notپرسەکان سەلامەت: ORM چی دەپارێزێت و چی ناپارێزێت

ORMs send values as bound parameters, the same defence you learned with prepared statements. Laravel's documentation says that strings passed to its query builder as bindings need no cleaning. Two things stay your job. First, a ? can only stand for a value, never for a column or a table name (what a ? cannot stand for). Second, every ORM has a door for raw SQL, such as whereRaw() and DB::raw(). Raw SQL glued together from user input is open to SQL injection.

نرخەکان وەک بەستراو دەنێرن، هەمان بەرگری کە لەگەڵ ڕستە ئامادەکراوەکان فێری بوویت. Laravel دەڵێت ئەو وەک بەستنەوە دەدرێنە ئامرازی دروستکردنی پێویستیان بە پاککردنەوە نییە. دوو شت هەر کاری تۆن. یەکەم، ? تەنیا دەتوانێت جێی نرخێک بگرێتەوە، هەرگیز جێی ناوی یان ناگرێتەوە (ئەوەی ? ناتوانێت جێی بگرێتەوە). دووەم، هەموو دەرگایەکی بۆ خاو هەیە، وەک whereRaw() و DB::raw(). خاو کە لە پێکەوە لکێنرابێت بۆ تێخزاندنی SQL کراوەیە.

Three rows. A value from the URL, Shilan, goes into WHERE name = ? and is sent apart from the SQL: always safe. A column name from the URL, 1 = 1 OR name, is glued into WHERE 1 = 1 OR name = ?: every row comes back. The same name passes an allow-list check, ['name', 'marks'], is not on the list and is refused. Bottom: ORMs bind values for you; names and raw SQL are still your job.سێ ڕیز. نرخێک لە URLـەوە، Shilan، دەچێتە ناو WHERE name = ? و جیا لە SQL دەنێردرێت: هەمیشە سەلامەتە. ناوی ستوونێک لە URLـەوە، 1 = 1 OR name، دەلکێنرێتە WHERE 1 = 1 OR name = ?: هەموو تۆمارەکان دەگەڕێنەوە. هەمان ناو بە پشکنینی لیستی ڕێگەپێدراو، ['name', 'marks']، تێدەپەڕێت، لە لیستەکەدا نییە و ڕەت دەکرێتەوە. لە خوارەوە: ORMـەکان نرخەکان بۆت دەبەستنەوە؛ ناوەکان و SQLـی خاو هێشتا کاری تۆن.
A value goes into a placeholder; a name must pass an allow-list.نرخ دەچێتە ناو ?؛ ناو دەبێت لە تێبپەڕێت.CêIT

Safe queries: what the ORM protects, and what it does notپرسەکان سەلامەت: ORM چی دەپارێزێت و چی ناپارێزێت

A common mistake: a column name from the URLهەڵەیەکی باو: ناوی ستوونێک لە URLـەوە

A findBy($column, $value) helper is handy, and many hand-made mappers have one. In the version below the value is bound, but the column name is glued into the SQL. Imagine that both come from the URL:

یارمەتیدەرێکی findBy($column, $value) بەسوودە و زۆر هاوتاکەری دەستکرد یەکێکیان هەیە. لەم وەشانەی خوارەوەدا نرخەکە دەبەسترێتەوە، بەڵام ناوی دەلکێنرێتە . وا دابنێ هەردووکیان لە URLـەوە دێن:

Unsafe and safe findBy()⁦findBy()⁩ـی ناسەلامەت و سەلامەتfind-by.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// find-by.php - a value can be bound; a column NAME cannot
$db = new PDO('sqlite::memory:');
$db->exec('CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT, marks INTEGER)');
$db->exec("INSERT INTO students (name, marks) VALUES
           ('Aram', 85), ('Shilan', 92), ('Karwan', 78)");

// WRONG: the column name is glued into the SQL text
function findByUnsafe(PDO $db, string $column, string $value): array
{
    $stmt = $db->prepare("SELECT name FROM students WHERE $column = ?");
    $stmt->execute([$value]);
    return $stmt->fetchAll(PDO::FETCH_COLUMN);
}

// RIGHT: only names from an allow-list may become part of the SQL
function findBy(PDO $db, string $column, string $value): array
{
    $allowed = ['name', 'marks'];
    if (!in_array($column, $allowed, true)) {
        throw new InvalidArgumentException("Cannot search by '$column'");
    }
    $stmt = $db->prepare("SELECT name FROM students WHERE $column = ?");
    $stmt->execute([$value]);
    return $stmt->fetchAll(PDO::FETCH_COLUMN);
}

// Imagine both values came from the URL: ?by=...&value=...
$by = "1 = 1 OR name";
$value = "nobody";

echo "Unsafe: ", implode(', ', findByUnsafe($db, $by, $value)), "\n";
try {
    $names = findBy($db, $by, $value);
    echo "Safe:   ", implode(', ', $names), "\n";
} catch (InvalidArgumentException $e) {
    echo "Safe:   refused - ", $e->getMessage(), "\n";
}
echo "Normal: ", implode(', ', findBy($db, 'marks', '92')), "\n";
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 11

Wrong: the column name becomes part of the SQL text.

هەڵە: ناوی دەبێتە بەشێک لە دەقی .

Lineدێڕی 19-21

Right: only names on the allow-list may enter the SQL.

دروست: تەنیا ناوەکانی ناو دەتوانن بچنە ناو .

The unsafe version returned every student, although nobody is called nobody: 1 = 1 OR name = ? is always true. The safe version refused the name before any SQL ran. An ORM cannot do this check for you, because only you know which columns may be searched.

وەشانە ناسەلامەتەکە هەموو قوتابییەکانی گەڕاندەوە، هەرچەندە کەس ناوی nobody نییە: 1 = 1 OR name = ? هەمیشە ڕاستە. وەشانە سەلامەتەکە ناوەکەی ڕەت کردەوە پێش ئەوەی هیچ جێبەجێ بێت. ناتوانێت ئەم پشکنینەت بۆ بکات، چونکە تەنیا تۆ دەزانیت کام دەکرێت بگەڕێندرێن.

The N+1 query problemکێشەی پرسی ⁦N+1⁩

With an ORM, $student->department->name looks like a simple property. Behind it, many ORMs load the linked object only when you first touch it: this is lazy loading. In a loop over 30 students that means 1 query for the list and then 1 more query for each student: 31 queries where 2 would do. This is the N+1 query problem, a very common reason for slow ORM pages. Eager loading fixes it: load all the linked rows at once with WHERE id IN (...).

لە $student->department->name وەک سادە دەردەکەوێت. لە پشتیەوە، زۆر ئەو پێیەوە بەستراوە تەنیا ئەو کاتە دەهێنن کە یەکەم جار دەستی لێ دەدەیت: ئەمە . لە بەسەر ⁦30⁩ قوتابیدا ئەمە واتە 1 بۆ لیستەکە و پاشان 1 تر بۆ هەر قوتابییەک: ⁦31⁩ لە کاتێکدا 2 بەس بوو. ئەمە ـە، هۆکارێکی زۆر باوی خاوبوونی . چارەسەری دەکات: هەموو بەستراوەکان پێکەوە بە WHERE id IN (...) بهێنە.

An animation in eight steps. Lazy loading: the PHP page sends one query for 30 students, then one query for the department of student 1, of student 2, of student 3 and so on, and the counter climbs to 31. Eager loading: one query for the students and one query for all their departments with WHERE id IN (2, 3, 1); the counter stops at 2. Last frame: 31 queries in 94 ms against 2 queries in 7 ms, measured with PHP 8.2 and SQLite and a 2 ms wait per trip; without the wait, 0.6 ms and 0.4 ms.
An animation in eight steps. Lazy loading: the PHP page sends one query for 30 students, then one query for the department of student 1, of student 2, of student 3 and so on, and the counter climbs to 31. Eager loading: one query for the students and one query for all their departments with WHERE id IN (2, 3, 1); the counter stops at 2. Last frame: 31 queries in 94 ms against 2 queries in 7 ms, measured with PHP 8.2 and SQLite and a 2 ms wait per trip; without the wait, 0.6 ms and 0.4 ms.
Lazy loading counts up to 31; eager loading stops at 2. تا ⁦31⁩ دەژمێرێت؛ لە 2 دەوەستێت.CêIT

The N+1 query problemکێشەی پرسی ⁦N+1⁩

Measured: 31 queries or 2پێوانەکراو: ⁦31⁩ پرس یان 2

This script runs both versions with plain PDO and SQLite, counts the queries and times them. To behave like a real database on another computer, each query first waits 2 ms, a short trip over the network.

ئەم هەردوو وەشانەکە بە PDOـی سادە و جێبەجێ دەکات، دەژمێرێت و کاتیان دەپێوێت. بۆ ئەوەی وەک ڕاستەقینە لەسەر تر ڕەفتار بکات، هەر سەرەتا ⁦2 ms⁩ چاوەڕێ دەکات، گەشتێکی کورت بە .

Counting the queriesژماردنی پرسەکانn-plus-one.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// n-plus-one.php - count the queries: lazy loading (N+1) or eager loading (2)

// A tiny database helper that counts every query and waits 2 ms for each one,
// as if the database were another computer on the network.
final class Db
{
    public int $queries = 0;

    public function __construct(private PDO $pdo, private int $delayMicroseconds = 2000) {}

    public function all(string $sql, array $params = []): array
    {
        $this->queries++;
        usleep($this->delayMicroseconds);          // the simulated network trip
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT)');
$pdo->exec('CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT, department_id INTEGER)');
$pdo->exec("INSERT INTO departments (name) VALUES ('IT'), ('Networks'), ('Design')");
$add = $pdo->prepare('INSERT INTO students (name, department_id) VALUES (?, ?)');
for ($i = 1; $i <= 30; $i++) {
    $add->execute(["Student $i", ($i % 3) + 1]);     // 30 students in 3 departments
}

// 1. Lazy loading: one query for the students, then one more for EACH student
$db = new Db($pdo);
$start = hrtime(true);
foreach ($db->all('SELECT id, name, department_id FROM students') as $s) {
    $dep = $db->all('SELECT name FROM departments WHERE id = ?', [$s['department_id']]);
    $line = "{$s['name']} - {$dep[0]['name']}";      // what the page would print
}
printf("Lazy:  %2d queries, %5.1f ms\n", $db->queries, (hrtime(true) - $start) / 1e6);

// 2. Eager loading: the students, then ALL their departments in one query
$db = new Db($pdo);
$start = hrtime(true);
$students = $db->all('SELECT id, name, department_id FROM students');
$ids = array_values(array_unique(array_column($students, 'department_id')));
$marks = implode(', ', array_fill(0, count($ids), '?'));        // "?, ?, ?"
$departments = array_column(
    $db->all("SELECT id, name FROM departments WHERE id IN ($marks)", $ids), 'name', 'id');
foreach ($students as $s) {
    $line = "{$s['name']} - {$departments[$s['department_id']]}";
}
printf("Eager: %2d queries, %5.1f ms\n", $db->queries, (hrtime(true) - $start) / 1e6);
echo "Last line printed: $line\n";
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 12-18

Every query is counted and waits 2 ms, like a trip to another computer.

هەموو دەژمێردرێت و ⁦2 ms⁩ چاوەڕێ دەکات، وەک گەشتێک بۆ تر.

Lineدێڕی 34-36

Lazy: one more query inside the loop, for every student.

: زیاتر لە ناو ، بۆ هەر قوتابییەک.

Lineدێڕی 44-47

Eager: collect the department ids, then one IN (?, ?, ?) query for all of them.

: idـی بەشەکان کۆ بکەرەوە، پاشان یەک IN (?, ?, ?) بۆ هەموویان.

31 queries took about 94 ms and 2 queries about 7 ms. Without the 2 ms wait, the same script printed 0.6 ms and 0.4 ms: on your own laptop the problem hides, and it appears when the database is on another computer and the list grows.

⁦31⁩ نزیکەی ⁦94 ms⁩ و 2 نزیکەی ⁦7 ms⁩ـی خایاند. بەبێ چاوەڕوانیی ⁦2 ms⁩، هەمان ⁦0.6 ms⁩ و ⁦0.4 ms⁩ـی چاپ کرد: لەسەر لاپتۆپی خۆت کێشەکە دەشاردرێتەوە و کاتێک دەردەکەوێت کە لەسەر تر بێت و لیستەکە گەورە ببێت.

The N+1 query problemکێشەی پرسی ⁦N+1⁩

Eager loading in an ORMبارکردنی پێشوەختە لە ORMـدا

ORMs do the same with one call. In Eloquent, a relation is a method on the model, and with() loads that relation for the whole list (not run here; it follows the Laravel 13 documentation):

هەمان کار بە یەک بانگکردن دەکەن. لە Eloquentـدا لەسەر مۆدێلەکە و with() ئەو بۆ هەموو لیستەکە دەهێنێت (لێرە جێبەجێ نەکراوە؛ بەپێی ⁦Laravel 13⁩):

with(): eager loading in Eloquent (not run)⁦with()⁩: بارکردنی پێشوەختە لە Eloquentـدا (جێبەجێ نەکراوە)PHP
<?php
// Laravel 13: a relation and eager loading (not run in this lesson)
class Student extends Model
{
    public function department(): BelongsTo     // students.department_id -> departments.id
    {
        return $this->belongsTo(Department::class);
    }
}

$students = Student::with('department')->get();  // 2 queries, not 31
foreach ($students as $student) {
    echo $student->name, ' - ', $student->department->name, "\n";
}
The code Student::with('department')->get(); points to two queries: query 1, select * from students, and query 2, select * from departments where id in (2, 3, 1), marked as simplified. Below: two queries for 30 students, or for 3,000. Model::preventLazyLoading() makes a forgotten lazy load an error while you develop.کۆدی Student::with('department')->get(); ئاماژە بە دوو پرس دەکات: پرس 1، select * from students و پرس 2، select * from departments where id in (2, 3, 1)، کە سادەکراوە. لە خوارەوە: دوو پرس بۆ 30 قوتابی، یان بۆ 3,000. Model::preventLazyLoading() بارکردنی دواخراوی لەبیرکراو لە کاتی گەشەپێداندا دەکات بە هەڵە.
with() turns one call into two queries, whatever the size of the list.⁦with()⁩ یەک بانگکردن دەکات بە دوو ، لیستەکە هەر قەبارەیەکی هەبێت.CêIT

While you develop, Model::preventLazyLoading(), called in the boot() method of AppServiceProvider, turns every forgotten lazy load into an exception, so you find N+1 loops before your users do.

لە کاتی گەشەپێداندا، Model::preventLazyLoading()، کە لە boot()ـی AppServiceProviderـدا بانگ دەکرێت، هەموو لەبیرکراو دەکات بە ، بۆیە ⁦N+1⁩ دەدۆزیتەوە پێش ئەوەی بیاندۆزنەوە.

Migrations in one screenمایگرەیشنەکان لە یەک شاشەدا

Tables change as a project grows: a new column, a new table. If each developer changes the tables by hand in phpMyAdmin, the copies on different computers and on the real server drift apart. A migration is a small file that makes one change in up() and undoes it in down(). Migrations run in the order of their names, and a migrations table in the database remembers which ones have run. Laravel's documentation compares them to version control for your database.

لەگەڵ گەشەکردنی پرۆژەدا دەگۆڕێن: نوێ، نوێ. ئەگەر هەر گەشەپێدەرێک بە دەست لە بگۆڕێت، کۆپییەکان لەسەر و لەسەر ڕاستەقینە لە یەکتر دوور دەکەونەوە. بچووکە کە لە up()ـدا یەک گۆڕانکاری دەکات و لە down()ـدا دەیگەڕێنێتەوە. بەپێی ڕیزی ناوەکانیان جێبەجێ دەکرێن و بە ناوی migrations لە لە بیری دەمێنێت کامیان جێبەجێ کراون. Laravel بەراوردیان دەکات بە بۆ .

A Laravel migration (not run)مایگرەیشنێکی Laravel (جێبەجێ نەکراوە)2026_11_01_000001_create_students_table.phpPHP
<?php
// database/migrations/2026_11_01_000001_create_students_table.php
// Laravel 13 (not run in this lesson)
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void              // one step forward
    {
        Schema::create('students', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->integer('marks');
            $table->timestamps();           // created_at and updated_at
        });
    }

    public function down(): void            // the same step backwards
    {
        Schema::drop('students');
    }
};
The Artisan commands (not run)فەرمانەکانی Artisan (جێبەجێ نەکراون)BASHType in a terminalلە تێرمیناڵدا بینووسە
php artisan make:migration create_students_table   # make a new, empty migration file
php artisan migrate                               # run every migration that has not run yet
php artisan migrate:rollback                      # undo the last batch
A timeline with three migration files in order. 2026_11_01_000001 create_students_table: up() CREATE TABLE students, down() DROP TABLE students. 2026_11_03_000002 add_city_to_students: up() ADD COLUMN city, down() DROP COLUMN city. 2026_11_09_000003 create_departments_table: up() CREATE TABLE departments, down() DROP TABLE departments. An arrow to the right is php artisan migrate; a shorter arrow back is php artisan migrate:rollback. A migrations table remembers what has run.هێڵێکی کات بە سێ فایلی مایگرەیشن بە ڕیز. 2026_11_01_000001 create_students_table: up() CREATE TABLE students، down() DROP TABLE students. 2026_11_03_000002 add_city_to_students: up() ADD COLUMN city، down() DROP COLUMN city. 2026_11_09_000003 create_departments_table: up() CREATE TABLE departments، down() DROP TABLE departments. تیرێک بەرەو ڕاست php artisan migrateـە؛ تیرێکی کورتتر بۆ دواوە php artisan migrate:rollbackـە. خشتەیەک بە ناوی migrations لە بیری دەمێنێت چی جێبەجێ کراوە.
Each migration is one step forward (up) with its own way back (down).هەر هەنگاوێکە بۆ پێشەوە ⁦(up)⁩ لەگەڵ ڕێگای گەڕانەوەی خۆی ⁦(down)⁩.CêIT

When plain SQL is betterکەی SQLـی سادە باشترە

An ORM is a tool, not a rule. Martin Fowler, who named the two patterns, wrote in 2012 that an ORM handles most of the mapping work (he guessed 80 to 90 per cent) and that the rest needs someone who knows the database well. That is why every ORM has a door to plain SQL. Write the SQL yourself, through PDO or through that door, when:

ئامرازێکە، نەک یاسایەک. Martin Fowler، کە ناوی ئەو دوو شێوازەی دانا، لە ⁦2012⁩ـدا نووسی کە زۆربەی کاری هاوتاکردن دەکات (مەزەندەی ⁦80⁩ تا ⁦90⁩ لە سەدی کرد) و ئەوەی دەمێنێتەوە کەسێکی پێویستە کە باش بناسێت. بۆیە هەموو دەرگایەکی بۆ سادە هەیە. خۆت بنووسە، بە PDO یان بەو دەرگایە، کاتێک:

  • you build a report with JOIN, GROUP BY and SUM (SQL functions and grouping);
  • you change thousands of rows: one UPDATE ... WHERE instead of loading 10,000 objects and saving them one by one;
  • a page is slow and you have measured the queries it sends;
  • you need a feature that only your database has.
  • بە JOIN، GROUP BY و SUM دروست دەکەیت (کردارەکانی SQL و کۆکردنەوە)؛
  • هەزاران دەگۆڕیت: یەک UPDATE ... WHERE لەبری هێنانی ⁦10,000⁩ و هەڵگرتنیان یەک یەک؛
  • خاوە و ئەو دەینێرێت پێوانەت کردوون؛
  • پێویستیت بە تایبەتمەندییەکە کە تەنیا هەیەتی.

For a small script, PDO alone, as in SQLite Basics, is often simpler than a whole framework.

بۆ بچووک، تەنیا PDO، وەک لە بنەماکانی SQLiteـدا، زۆرجار لە تەواو سادەترە.

Two columns. Let the ORM do it: find, save and delete one record; objects and their relations; forms and validation in a framework; one code for MySQL, SQLite and PostgreSQL. Write the SQL yourself: reports with JOIN, GROUP BY and SUM; one UPDATE for 10,000 rows; a slow query that you measured; a feature of one database only. Below: most projects use both, the ORM for everyday work and SQL where it is clearer or faster.دوو ستوون. با ORM بیکات: هێنان، هەڵگرتن و سڕینەوەی یەک تۆمار؛ ئۆبجێکتەکان و پەیوەندییەکانیان؛ فۆرمەکان و پشتڕاستکردنەوە لە فرەیموۆرکدا؛ یەک کۆد بۆ MySQL، SQLite و PostgreSQL. SQL خۆت بنووسە: ڕاپۆرت بە JOIN، GROUP BY و SUM؛ یەک UPDATE بۆ 10,000 تۆمار؛ پرسێکی خاو کە پێوانەت کردووە؛ تایبەتمەندییەکی تەنیا یەک بنکەدراوە. لە خوارەوە: زۆربەی پرۆژەکان هەردووکیان بەکار دەهێنن، ORM بۆ کاری ڕۆژانە و SQL لەو شوێنانەی ڕوونتر یان خێراترە.
Use both: the ORM for everyday work, SQL where it is clearer or faster.هەردووکیان بەکار بهێنە: بۆ کاری ڕۆژانە، لەو شوێنانەی ڕوونتر یان خێراترە.CêIT

Common mistakesهەڵە باوەکان

Wrong: an id before the INSERTهەڵە: id پێش INSERTmistake-id.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// mistake-id.php - an object that is not saved yet has no id
class Student
{
    public int $id;                        // no "?" and no default value

    public function __construct(public string $name, public int $marks) {}
}

$hana = new Student('Hana', 90);
echo "Hana's id: {$hana->id}\n";           // read before any INSERT gave it an id
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 5

int without ? and without a default: the property has no value at all until something sets it.

int بەبێ ? و بەبێ : هیچ نرخێکی نییە تا شتێک دایبنێت.

Fix: declare public ?int $id = null;, as StudentMapper does, and check $student->id === null before you use the id.

چارەسەر: public ?int $id = null; بنووسە، وەک ئەوەی StudentMapper دەیکات. پێش بەکارهێنانی id، $student->id === null بپشکنە.

Exercisesڕاهێنانەکان

Exerciseڕاهێنان

Rewrite the mapper in the Active Record style. Give Student a static PDO $db, a save() that inserts or updates the object itself and a static find(int $id). Test it: save Karwan with 78 marks, change his marks to 81, save again and find him.

هاوتاکەرەکە بە شێوازی بنووسەوە. Student PDO $dbـێکی static، save()ـێک کە خودی INSERT یان UPDATE دەکات و find(int $id)ـێکی staticـی پێ بدە. تاقیی بکەرەوە: Karwan بە ⁦78⁩ نمرە هەڵبگرە، نمرەکەی بکە بە ⁦81⁩، دیسان هەڵیبگرە و بیدۆزەرەوە.

Hintئاماژە

Move the code of StudentMapper::save() into Student; use self::$db for the connection (static members) and $this for the object.

کۆدی StudentMapper::save() بگوازەرەوە بۆ ناو Student؛ بۆ پەیوەندییەکە self::$db (ئەندامە staticـەکان) و بۆ $this بەکار بهێنە.

Show answerوەڵامەکە پیشان بدە
active-record.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// active-record.php - the same job in the Active Record style: the object saves itself

class Student
{
    public static PDO $db;                  // one connection for the whole class
    public ?int $id = null;

    public function __construct(public string $name, public int $marks) {}

    public function save(): void
    {
        if ($this->id === null) {
            $stmt = self::$db->prepare('INSERT INTO students (name, marks) VALUES (?, ?)');
            $stmt->execute([$this->name, $this->marks]);
            $this->id = (int) self::$db->lastInsertId();
        } else {
            $stmt = self::$db->prepare('UPDATE students SET name = ?, marks = ? WHERE id = ?');
            $stmt->execute([$this->name, $this->marks, $this->id]);
        }
    }

    public static function find(int $id): ?Student
    {
        $stmt = self::$db->prepare('SELECT id, name, marks FROM students WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return null;
        }
        $student = new Student($row['name'], (int) $row['marks']);
        $student->id = (int) $row['id'];
        return $student;
    }
}

Student::$db = new PDO('sqlite::memory:');
Student::$db->exec('CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT, marks INTEGER)');

$karwan = new Student('Karwan', 78);
$karwan->save();                            // INSERT
$karwan->marks = 81;
$karwan->save();                            // UPDATE

$again = Student::find($karwan->id);
echo "{$again->id}: {$again->name} has {$again->marks} marks\n";
Exerciseڕاهێنان

Add a method delete(Student $student) to StudentMapper. It should do nothing for an object that was never saved, and it should set the object's id back to null after the DELETE.

delete(Student $student) بۆ StudentMapper زیاد بکە. بۆ کە هەرگیز هەڵنەگیراوە نابێت هیچ بکات و دوای DELETE دەبێت idـی بکاتەوە بە null.

Show answerوەڵامەکە پیشان بدە
Add inside class StudentMapperلە ناو class StudentMapperـدا زیادی بکەPHP
    public function delete(Student $student): void
    {
        if ($student->id === null) {
            return;                                // never saved: nothing to delete
        }
        $this->run('DELETE FROM students WHERE id = ?', [$student->id]);
        $student->id = null;                       // the object is "new" again
    }
test-delete.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
require __DIR__ . '/mapper.php';

$db = new PDO('sqlite::memory:');
$db->exec('CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT NOT NULL, marks INTEGER NOT NULL)');
$mapper = new StudentMapper($db);

$dilan = new Student('Dilan', 70);
$mapper->save($dilan);
$mapper->delete($dilan);
echo "Rows left: ", $db->query('SELECT COUNT(*) FROM students')->fetchColumn(), "\n";
var_dump($dilan->id);
Exerciseڕاهێنان

A shop page lists 40 orders. For each order it prints the customer's name, and the ORM loads the customer lazily. (a) How many queries does the page send? (b) How many with eager loading? (c) If every query costs a 5 ms trip to the database server, how much time does eager loading save?

فرۆشگا ⁦40⁩ داواکاریی کڕین لیست دەکات. بۆ هەر داواکارییەک ناوی کڕیارەکە چاپ دەکات و کڕیارەکە بە دەهێنێت. (أ) چەند دەنێرێت؟ (ب) بە چەند؟ (ج) ئەگەر هەر گەشتێکی ⁦5 ms⁩ـی بۆ بخایەنێت، چەند کات دەگەڕێنێتەوە؟

Show answerوەڵامەکە پیشان بدە

(a) 1 query for the orders + 40 queries for the customers = 41. (b) 2: the orders, then all their customers with WHERE id IN (...). (c) The saving:

(أ) 1 بۆ داواکارییەکان + ⁦40⁩ بۆ کڕیارەکان = ⁦41⁩. (ب) 2: داواکارییەکان، پاشان هەموو کڕیارەکانیان بە WHERE id IN (...). (ج) کاتی گەڕاوە:

41×5 ms−2×5 ms=205 ms−10 ms=195 ms41 \times 5\,\text{ms} - 2 \times 5\,\text{ms} = 205\,\text{ms} - 10\,\text{ms} = 195\,\text{ms}

On every visit to the page, before any other work.

لە هەر سەردانێکی ، پێش هەر کارێکی تر.

Exerciseڕاهێنان

Active Record or Data Mapper? (a) $order->save(); (b) $em->persist($order); $em->flush(); (c) $orderMapper->insert($order); (d) Order::find(7)->delete();

یان ؟ (أ) $order->save(); (ب) $em->persist($order); $em->flush(); (ج) $orderMapper->insert($order); (د) Order::find(7)->delete();

Show answerوەڵامەکە پیشان بدە
  • (a) Active Record: the order saves itself.
  • (b) Data Mapper: Doctrine's EntityManager saves it.
  • (c) Data Mapper: a separate mapper object.
  • (d) Active Record: the class finds and deletes its own row.
  • (أ) : داواکارییەکە خۆی هەڵدەگرێت.
  • (ب) : EntityManagerـی Doctrine هەڵیدەگرێت.
  • (ج) : هاوتاکەری جیا.
  • (د) : خۆی دەدۆزێتەوە و دەیسڕێتەوە.
Exerciseڕاهێنان

Make this line safe: $rows = $db->query("SELECT name FROM students ORDER BY " . $_GET['sort']);

ئەم دێڕە سەلامەت بکە: $rows = $db->query("SELECT name FROM students ORDER BY " . $_GET['sort']);

Show answerوەڵامەکە پیشان بدە

A column name cannot be a bound ?, so check it against an allow-list and use a safe default. In a page you would call sortedNames($db, $_GET['sort'] ?? '').

ناوی ناتوانێت ?ـی بەستراو بێت، بۆیە بە بیپشکنە و سەلامەت بەکار بهێنە. لە بەم شێوەیە بانگی دەکەیت: sortedNames($db, $_GET['sort'] ?? '').

safe-sort.phpPHPRuns on the serverلەسەر ڕاژەکاری وێب کار دەکات
<?php
// safe-sort.php - the sort column must come from an allow-list
$db = new PDO('sqlite::memory:');
$db->exec('CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT, marks INTEGER)');
$db->exec("INSERT INTO students (name, marks) VALUES ('Aram', 85), ('Shilan', 92), ('Karwan', 78)");

function sortedNames(PDO $db, string $wanted): string
{
    $allowed = ['name', 'marks'];
    $sort = in_array($wanted, $allowed, true) ? $wanted : 'name';   // unknown: use name
    $rows = $db->query("SELECT name FROM students ORDER BY $sort")->fetchAll(PDO::FETCH_COLUMN);
    return implode(', ', $rows);
}

echo "sort=marks:     ", sortedNames($db, 'marks'), "\n";
echo "sort=(attack):  ", sortedNames($db, "name; DROP TABLE students"), "\n";
echo "Rows still in the table: ", $db->query('SELECT COUNT(*) FROM students')->fetchColumn(), "\n";
Exerciseڕاهێنان

Write the up() and down() of a Laravel migration that adds a phone column, which may be empty, to students.

up() و down()ـی Laravel بنووسە کە phone زیاد دەکات بۆ students، کە دەتوانێت بەتاڵ بێت.

Show answerوەڵامەکە پیشان بدە
One possible answer (Laravel 13 documentation; not run)وەڵامێکی گونجاو (بەڵگەنامەی ⁦Laravel 13⁩؛ جێبەجێ نەکراوە)PHP
<?php
// Laravel 13 (not run in this lesson)
public function up(): void
{
    Schema::table('students', function (Blueprint $table) {
        $table->string('phone')->nullable();     // may be empty
    });
}

public function down(): void
{
    Schema::table('students', function (Blueprint $table) {
        $table->dropColumn('phone');
    });
}

Schema::table() changes an existing table, nullable() allows an empty value, and dropColumn() undoes the change.

Schema::table() هەبوو دەگۆڕێت، nullable() ڕێگە بە نرخی بەتاڵ دەدات و dropColumn() گۆڕانکارییەکە دەگەڕێنێتەوە.

Summaryپوختە

  • Databases keep rows, PHP code wants objects: identity, links, types and inheritance differ between the two worlds.
  • An ORM maps classes to tables, objects to rows, properties to columns and object links to foreign keys, and writes the SQL.
  • Active Record: the object saves itself (Eloquent). Data Mapper: a mapper saves plain objects (Doctrine, our StudentMapper).
  • ORMs bind values; column names, raw SQL and mass assignment are still your job.
  • N+1: lazy loading in a loop sends 1 + N queries (31 instead of 2, measured); eager loading (with()) fixes it.
  • Migrations change the schema step by step with up() and down(); use plain SQL for reports, bulk changes and measured slow queries.
  • هەڵدەگرن و کۆدی PHP دەوێت: ناسنامە، بەستنەوە، جۆر و لە نێوان هەردوو جیهانەکەدا جیاوازن.
  • بە ، بە ، بە و بەستنەوەی بە هاوتا دەکات و دەنووسێت.
  • : خۆی هەڵدەگرێت ⁦(Eloquent)⁩. : هاوتاکەرێک سادە هەڵدەگرێت (Doctrine، StudentMapperـەکەی ئێمە).
  • نرخەکان دەبەستنەوە؛ ناوی ، خاو و هێشتا کاری تۆن.
  • ⁦N+1⁩: لە ناو ⁦1 + N⁩ دەنێرێت (⁦31⁩ لەبری 2، پێوانەکراو)؛ (with()) چارەسەری دەکات.
  • هەنگاو بە هەنگاو بە up() و down() دەگۆڕن؛ بۆ ، گۆڕانکاریی بەکۆمەڵ و خاوی پێوانەکراو سادە بەکار بهێنە.

Next: the web remembers its visitors with cookies and sessions.

دواتر: وێب سەردانکەرانی خۆی بە کووکییەکان و سێشنەکان دەناسێتەوە.

Lesson complete!وانەکە تەواو بوو!

Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.

Key termsزاراوە سەرەکییەکان

Next lessonوانەی داهاتوو

Cookiesکووکییەکان

References and further readingسەرچاوەکان و خوێندنەوەی زیاتر

This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.

Appearanceڕووکار

Modeدۆخ
Styleشێواز
Coloursڕەنگەکان

Saved on this device only.تەنیا لەسەر ئەم ئامێرە پاشەکەوت دەکرێت.