
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:
- Map database tables to PHP classes – Each table becomes a class, each row an object instance
- Handle database operations through PHP objects – No more SQL (mostly)
- Work with multiple database systems – MySQL, PostgreSQL, SQLite, and more
- Manage complex relationships – One-to-one, one-to-many, many-to-many relationships become simple
- 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" => "[email protected]",
"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" => "[email protected]",
"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:
Discover more from CodeSamplez.com
Subscribe to get the latest posts sent to your email.

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.