Development

PHP Doctrine ORM: The Ultimate Beginners Guide

Let me tell you something – when I first encountered Doctrine ORM, I was completely overwhelmed. The documentation seemed written for people who already understood ORM concepts, and I was lost in a sea of unfamiliar terms. Fast forward a bit, I couldn’t stop using Doctrine in almost every PHP project I build. It’s that powerful.

Doctrine has absolutely revolutionized how I work with databases in PHP applications. No more writing repetitive SQL queries or dealing with complex database operations manually – Doctrine handles all that heavy lifting for me. And today, I’m going to show you exactly how to perform basic CRUD (Create, Read, Update, Delete) operations with Doctrine that will transform your PHP development experience forever.

What is Doctrine ORM?

Doctrine is the most powerful Object-Relational Mapping (ORM) tool available for PHP. It creates a virtual object database that you can use from within PHP, eliminating the need to write SQL in most cases. Instead of thinking in tables and rows, you’ll work with PHP objects and their properties.

The beauty of Doctrine lies in its ability to:

  1. Map database tables to PHP classes – Each table becomes a class, each row an object instance
  2. Handle database operations through PHP objects – No more SQL (mostly)
  3. Work with multiple database systems – MySQL, PostgreSQL, SQLite, and more
  4. Manage complex relationships – One-to-one, one-to-many, many-to-many relationships become simple
  5. Improve code organization – Entity classes represent your data structure

Getting Started with Doctrine

Before diving into CRUD operations, let’s make sure you’ve got Doctrine properly installed and configured.

Installation

The easiest way to install Doctrine is through Composer. If you don’t have Composer yet, download it here.

composer require doctrine/ormCode language: JavaScript (javascript)

This single command will install Doctrine ORM and all its dependencies for you. Simple!

Basic Configuration

To use Doctrine, you’ll need to set up entity manager configuration. Here’s a basic setup:

// bootstrap.php
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;

// Create a simple "default" Doctrine ORM configuration for Annotations
$config = ORMSetup::createAttributeMetadataConfiguration(
    [__DIR__."/src"],
    true
);

// Database configuration parameters
$dbParams = [
    'driver'   => 'pdo_mysql',
    'user'     => 'db_username',
    'password' => 'db_password',
    'dbname'   => 'database_name',
];

// Obtaining the entity manager
$entityManager = EntityManager::create($dbParams, $config);Code language: PHP (php)

Creating Your First Entity

Let’s create a simple Contact entity class:

// src/Entity/Contact.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: "contacts")]
class Contact
{
    #[ORM\Id]
    #[ORM\Column(type: "integer")]
    #[ORM\GeneratedValue]
    private $id;
    
    #[ORM\Column(type: "string", length: 255)]
    private $name;
    
    #[ORM\Column(type: "string", length: 255)]
    private $email;
    
    #[ORM\Column(type: "string", length: 255)]
    private $subject;
    
    #[ORM\Column(type: "text")]
    private $message;
    
    // Getters and setters
    public function getId() {
        return $this->id;
    }
    
    public function getName() {
        return $this->name;
    }
    
    public function setName($name) {
        $this->name = $name;
    }
    
    public function getEmail() {
        return $this->email;
    }
    
    public function setEmail($email) {
        $this->email = $email;
    }
    
    public function getSubject() {
        return $this->subject;
    }
    
    public function setSubject($subject) {
        $this->subject = $subject;
    }
    
    public function getMessage() {
        return $this->message;
    }
    
    public function setMessage($message) {
        $this->message = $message;
    }
}Code language: PHP (php)

With our setup complete, let’s dive into CRUD operations!

Doctrine CRUD Operations in Action

CREATE: Inserting Records into the Database

Creating and saving new records with Doctrine is beautifully straightforward. Instead of constructing SQL INSERT statements, you simply create a new object, set its properties, and persist it:

function save_contact($data, $id = null) {
    global $entityManager;
    
    if (empty($id)) {
        // Create new entity for insertion
        $contact = new \App\Entity\Contact();
    } else {
        // Load existing entity for update
        $contact = $entityManager->find("App\Entity\Contact", $id);
        if (!$contact) {
            return false;
        }
    }
    
    // Set properties
    $contact->setName($data["name"]);
    $contact->setEmail($data["email"]);
    $contact->setSubject($data["subject"]);
    $contact->setMessage($data["message"]);
    
    try {
        // Save to database
        $entityManager->persist($contact);
        $entityManager->flush();
        return true;
    } catch (\Exception $err) {
        // Handle error
        return false;
    }
}Code language: PHP (php)

Usage is super simple:

$data = [
    "name" => "John Doe",
    "email" => "john@example.com",
    "subject" => "Hello",
    "message" => "This is a test message"
];

save_contact($data); // For insertionCode language: PHP (php)

The beauty of this approach is that the save_contact function handles both creation AND updating – no duplicate code needed!

READ: Retrieving Data from the Database

Reading data is where Doctrine truly shines. Let’s look at a few different ways to retrieve data:

Finding a Single Record by ID

function get_single($id) {
    global $entityManager;
    
    try {
        $contact = $entityManager->find("App\Entity\Contact", $id);
        return $contact;
    } catch (\Exception $err) {
        return null;
    }
}Code language: PHP (php)

It doesn’t get simpler than that! The find() method automatically looks up the primary key column (in this case, ‘id’).

Getting Records with Filtering, Sorting, and Pagination

When building real applications, you’ll frequently need to retrieve multiple records with various conditions:

function get_contacts($start = 0, $limit = 10, $criteria = null, $orderBy = null) {
    global $entityManager;
    
    try {
        return $entityManager->getRepository("App\Entity\Contact")
            ->findBy($criteria, $orderBy, $limit, $start);
    } catch (\Exception $err) {
        return [];
    }
}Code language: PHP (php)

Usage examples:

// Get all active contacts, ordered by name
$activeContacts = get_contacts(
    0,                  // start index
    10,                 // limit
    ['active' => true], // criteria
    ['name' => 'ASC']   // order by
);

// Get the 5 most recent contacts
$recentContacts = get_contacts(
    0,                    // start index
    5,                    // limit
    null,                 // no criteria
    ['createdAt' => 'DESC'] // order by date descending
);Code language: PHP (php)

Counting Total Records

Counting is essential for pagination. Here’s how to get the total count:

function get_contact_count($criteria = null) {
    global $entityManager;
    
    try {
        $queryBuilder = $entityManager->createQueryBuilder()
            ->select('COUNT(c)')
            ->from('App\Entity\Contact', 'c');
            
        // Add criteria if provided
        if ($criteria) {
            foreach ($criteria as $field => $value) {
                $queryBuilder->andWhere("c.$field = :$field")
                    ->setParameter($field, $value);
            }
        }
        
        return $queryBuilder->getQuery()->getSingleScalarResult();
    } catch (\Exception $err) {
        return 0;
    }
}Code language: PHP (php)

UPDATE: Modifying Existing Records

As I mentioned earlier, our save_contact function actually handles both creation and updates. Here’s how you’d use it for updates:

$data = [
    "name" => "John Doe Updated",
    "email" => "john.updated@example.com",
    "subject" => "Updated Subject",
    "message" => "This message has been updated"
];

save_contact($data, 1); // The '1' is the ID to updateCode language: PHP (php)

The function will load the existing contact, update its properties, and save the changes. So elegant!

DELETE: Removing Records from the Database

Deleting records with Doctrine is straightforward:

function delete_contacts($ids) {
    global $entityManager;
    
    try {
        // Convert to array if single ID
        if (!is_array($ids)) {
            $ids = [$ids];
        }
        
        foreach ($ids as $id) {
            // Performance optimization: Use partial reference
            $contact = $entityManager->getPartialReference("App\Entity\Contact", $id);
            $entityManager->remove($contact);
        }
        
        // Execute all deletions at once
        $entityManager->flush();
        return true;
    } catch (\Exception $err) {
        return false;
    }
}Code language: PHP (php)

The getPartialReference() method is a performance optimization that deserves special attention. Instead of loading the entire entity from the database (which would happen with find()), it creates a lightweight reference object that contains just the ID. This is perfect for deletion operations where you don’t need the entity’s data.

Advanced Doctrine Features

While basic CRUD operations cover most needs, Doctrine offers advanced features for complex scenarios:

Query Builder for Complex Queries

When the standard repository methods aren’t enough:

function find_contacts_by_keyword($keyword) {
    global $entityManager;
    
    $queryBuilder = $entityManager->createQueryBuilder();
    
    $query = $queryBuilder->select('c')
        ->from('App\Entity\Contact', 'c')
        ->where($queryBuilder->expr()->orX(
            $queryBuilder->expr()->like('c.name', ':keyword'),
            $queryBuilder->expr()->like('c.email', ':keyword'),
            $queryBuilder->expr()->like('c.subject', ':keyword'),
            $queryBuilder->expr()->like('c.message', ':keyword')
        ))
        ->setParameter('keyword', '%' . $keyword . '%')
        ->getQuery();
    
    return $query->getResult();
}Code language: PHP (php)

Native SQL Queries

Sometimes you need raw SQL power:

function execute_raw_query($sql, $params = []) {
    global $entityManager;
    
    $connection = $entityManager->getConnection();
    $statement = $connection->prepare($sql);
    
    return $statement->executeQuery($params)->fetchAllAssociative();
}Code language: PHP (php)

Performance Optimization Tips

Working with Doctrine in production? Here are some essential optimization tips:

  • Use batch processing for large operations:

// Process 100 records at a time 
$batchSize = 100; $i = 0; 
foreach ($largeDataset as $data) { 
    $contact = new Contact(); 
    // Set properties... 
    $entityManager->persist($contact); 
    // Flush every 100 iterations 
    if (($i % $batchSize) === 0) { 
        $entityManager->flush(); 
        $entityManager->clear(); 
        // Clear managed objects 
    } 
    $i++; 
} 
<em>// Flush remaining entities</em> 
$entityManager->flush();Code language: PHP (php)
  • Use partial objects when you don’t need all fields:
$query = $entityManager->createQuery('SELECT c.id, c.name FROM App\Entity\Contact c'); 
$partialContacts = $query->getResult();Code language: PHP (php)
  • Implement proper indexing on your database tables
  • Enable the query cache and metadata cache in production

Common Doctrine Pitfalls and Solutions

The N+1 Problem

One of the most common performance issues with ORMs is the N+1 query problem:

// BAD: Will execute N+1 queries
$contacts = $entityManager->getRepository(Contact::class)->findAll();
foreach ($contacts as $contact) {
    // This will execute a separate query for each contact
    $messages = $contact->getMessages();
}

// GOOD: Uses join to fetch related data in one query
$query = $entityManager->createQuery(
    'SELECT c, m FROM App\Entity\Contact c JOIN c.messages m'
);
$contacts = $query->getResult();Code language: PHP (php)

Detached Entity Exceptions

Ever seen this error? “A new entity was found through the relationship…”

Solution:

// Make sure to persist related entities first
$entityManager->persist($relatedEntity);
$entityManager->persist($mainEntity);
$entityManager->flush();Code language: PHP (php)

Conclusion

Doctrine has completely transformed how I build PHP applications. By abstracting away the database layer and letting me work with PHP objects, I can focus on building features rather than wrestling with SQL queries.

If you’re just starting with Doctrine, I hope this guide has given you a solid foundation. Remember, the initial learning curve might feel steep, but the productivity gains are absolutely worth it. Start with these basic CRUD operations, then gradually explore more advanced features.

Happy coding, and may your database interactions be forever simplified with the power of Doctrine ORM!

Want to learn more? Check out my other Doctrine tutorials:

Rana Ahsan

Rana Ahsan is a seasoned software engineer and technology leader specialized in distributed systems and software architecture. With a Master’s in Software Engineering from Concordia University, his experience spans leading scalable architecture at Coursera and TopHat, contributing to open-source projects. This blog, CodeSamplez.com, showcases his passion for sharing practical insights on programming and distributed systems concepts and help educate others. Github | X | LinkedIn

View Comments

  • God bless you dude. You saved my life. I looked from videos to official docs of the Doctrine, they all suck. But this saved me. Really great tutorial. Hope you can do same for PHPUnit too ;)

  • while performing select/read operation when i am trying to load retrieved data in view page following error occurs

    Fatal error: Cannot access private property PdContact::$name in C:\xampp\htdocs\doctrine\application\views\retrievedata.php on line 10

    how can i show them in view page.

Recent Posts

Service Workers in React: Framework Integration Guide

Learn how to integrate service workers in React, Next.js, Vue, and Angular with practical code examples and production-ready implementations for modern web applications.

2 weeks ago

Service Worker Caching Strategies: Performance & Offline Apps

Master the essential service worker caching strategies that transform web performance. Learn Cache-First, Network-First, and Stale-While-Revalidate patterns with practical examples that'll make your apps blazingly…

3 weeks ago

Service Worker Lifecycle: Complete Guide for FE Developers

Master the intricate dance of service worker states and events that power modern PWAs. From registration through installation, activation, and termination, understanding the lifecycle unlocks…

4 weeks ago

This website uses cookies.