PHP Destructor
What Is a Destructor? In simple terms: A destructor is a special method that runs automatically when an object is no longer needed and is about to be removed from memory. It is the object's chance to
What Is a Destructor?
In simple terms: A destructor is a special method that runs automatically when an object is no longer needed and is about to be removed from memory. It is the object's chance to "clean up after itself" — for example, closing a file, releasing a lock, or deleting a temporary file.
Technically: A destructor is a method named __destruct(). It takes no parameters. PHP calls it when the last reference to an object disappears — when the variable goes out of scope, is unset, or is overwritten — or, for objects still alive, during script shutdown. Objects involved in circular references are destroyed later by the garbage collector.
Constructor vs Destructor
| Constructor | Destructor | |
|---|---|---|
| Name | __construct() | __destruct() |
| Called when | The object is created with new | The object is destroyed |
| Parameters | Any | None |
| Typical job | Acquire data and resources | Release resources |
A Destructor in Action
<?php
class TempFile
{
private string $path;
public function __construct(string $name)
{
$this->path = sys_get_temp_dir() . '/' . $name . '.tmp';
file_put_contents($this->path, '');
echo "Created " . basename($this->path) . "\n";
}
public function write(string $data): void
{
file_put_contents($this->path, $data, FILE_APPEND);
}
public function __destruct()
{
if (is_file($this->path)) {
unlink($this->path);
}
echo "Deleted " . basename($this->path) . "\n";
}
}
function buildReport(): void
{
$temp = new TempFile('report');
$temp->write("Daily sales report\n");
echo "Processing report\n";
} // $temp goes out of scope here
buildReport();
echo "Back in the main script\n";
Output
Created report.tmp
Processing report
Deleted report.tmp
Back in the main script
How It Works
new TempFile('report')runs the constructor, which creates the file.- The object is stored only in the local variable
$temp. - When
buildReport()returns,$tempdisappears. The object has no remaining references, so PHP calls__destruct()immediately — before the next line of the main script runs. - The destructor deletes the temporary file, so cleanup cannot be forgotten, even if the function returns early.
PHP uses reference counting: each object knows how many variables refer to it. When the count drops to zero, the object is destroyed right away. This makes destruction timing predictable in most situations.
When Exactly Is the Destructor Called?
<?php
class Tracker
{
public function __construct(private string $name)
{
echo "Construct {$this->name}\n";
}
public function __destruct()
{
echo "Destruct {$this->name}\n";
}
}
$a = new Tracker('A');
$copy = $a; // second reference to the same object
unset($a);
echo "After unset(\$a)\n";
$copy = null;
echo "After \$copy = null\n";
$b = new Tracker('B');
$b = new Tracker('C'); // B loses its only reference
echo "End of script\n";
Output
Construct A
After unset($a)
Destruct A
After $copy = null
Construct B
Construct C
Destruct B
End of script
Destruct C
Step-by-Step Explanation
unset($a)removes one reference, but$copystill refers to object A, so A is not destroyed yet.$copy = nullremoves the last reference; A is destroyed immediately.$b = new Tracker('C')first creates C, then overwrites$b; B loses its only reference and is destroyed.- C is still alive when the script ends, so its destructor runs during shutdown.
| Event | Destructor Runs |
|---|---|
| Last variable referring to the object goes out of scope | Immediately |
unset($var) or $var = null on the last reference | Immediately |
Script ends normally, or exit() is called | During shutdown |
| Object is part of a circular reference | When the garbage collector runs |
| Fatal error, or the process is killed | Not guaranteed |
Circular References and Garbage Collection
If two objects refer to each other, their reference counts never reach zero on their own, even when no variable refers to them anymore:
<?php
class Node
{
public ?Node $partner = null;
public function __construct(private string $name) {}
public function __destruct()
{
echo "Destroying {$this->name}\n";
}
}
$a = new Node('A');
$b = new Node('B');
$a->partner = $b;
$b->partner = $a;
unset($a, $b);
echo "Variables unset\n";
gc_collect_cycles();
echo "Garbage collected\n";
Output (the order of the two "Destroying" lines may vary)
Variables unset
Destroying A
Destroying B
Garbage collected
After unset(), each object still holds a reference to the other, so neither destructor runs. PHP's cycle collector finds such unreachable groups periodically (or when gc_collect_cycles() is called) and destroys them. This is rarely a problem in short web requests, but it matters in long-running scripts and workers (see Garbage Collection).
Practical Example: Releasing a Lock Automatically
Destructors are well suited for releasing resources that were acquired in the constructor — a pattern known as RAII ("resource acquisition is initialization"):
<?php
final class FileLock
{
private $handle;
public function __construct(private string $path)
{
$this->handle = fopen($path, 'c');
if ($this->handle === false || !flock($this->handle, LOCK_EX | LOCK_NB)) {
throw new RuntimeException('Another process is already running.');
}
echo "Lock acquired\n";
}
public function __destruct()
{
if (is_resource($this->handle)) {
flock($this->handle, LOCK_UN);
fclose($this->handle);
echo "Lock released\n";
}
}
}
function runNightlyImport(string $lockFile): void
{
$lock = new FileLock($lockFile);
echo "Importing products...\n";
throw new RuntimeException('Supplier feed unavailable');
}
try {
runNightlyImport(sys_get_temp_dir() . '/import.lock');
} catch (RuntimeException $e) {
echo "Import failed: " . $e->getMessage() . "\n";
}
Output
Lock acquired
Importing products...
Lock released
Import failed: Supplier feed unavailable
Why This Works
- The constructor acquires an exclusive, non-blocking lock. If another copy of the import is already running, construction fails and the import never starts.
- When the exception leaves
runNightlyImport(), the local$lockvariable goes out of scope, and the destructor releases the lock before thecatchblock runs. - The lock is released whether the import succeeds, returns early, or fails — the cleanup is tied to the object's lifetime.
(Inside a catch block, the exception object can keep a reference to the function's variables through its stack trace in some situations; for critical resources, combining destructors with explicit try/finally cleanup is the safest approach.)
Rules and Limitations
- A destructor takes no parameters and its return value is ignored.
- If a child class defines a destructor, the parent's destructor is not called automatically; call
parent::__destruct()explicitly if needed (see PHP Inheritance). - Throwing an exception from a destructor during script shutdown causes a fatal error. Destructors should catch their own exceptions.
- Do not rely on destructors for essential work such as saving data or sending emails: after a fatal error or a killed process, they may never run.
- The order in which remaining objects are destroyed at shutdown should not be relied upon.
When You Need Explicit Cleanup
For resources that must be released at a precise moment, provide an explicit method (close(), release()) and call it in a finally block; keep the destructor as a safety net:
public function close(): void
{
if (is_resource($this->handle)) {
fclose($this->handle);
}
}
public function __destruct()
{
$this->close(); // safety net if close() was not called
}
Common Mistakes
Expecting Destruction While References Remain
Effect: unset($obj) does not trigger the destructor because another variable (or an array, or another object) still refers to the object.
Fix: Remember that only the last reference matters.
Doing Essential Work in Destructors
Effect: Data is lost when the script ends abnormally.
Fix: Save data explicitly; use destructors only for cleanup.
Throwing Exceptions From Destructors
Effect: Fatal errors during shutdown.
Fix: Catch and log exceptions inside the destructor.
Forgetting parent::__destruct()
Effect: Resources managed by the parent class are not released.
Fix: Call the parent destructor from overriding destructors.
Best Practices
- Use destructors to release resources acquired in the constructor (files, locks, temporary files).
- Keep destructors short, safe, and free of exceptions.
- Provide explicit cleanup methods for precise control, with the destructor as a fallback.
- Avoid circular references in long-running processes, or break them explicitly.
- Never depend on destructors for business-critical operations.
Official reference: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.destructor