File Delete / Rename / Copy
Besides reading and writing, applications regularly manage files: deleting temporary files, renaming uploads, moving files between folders, making backups, and rotating log files. PHP provides three core functions for this — unlink(), rename(), and copy() — plus functions for directories. This lesson covers how they behave, how they fail, and how to use them safely.
| Task | Function | Returns |
|---|---|---|
| Delete a file | unlink($path) | true / false |
| Rename or move a file or directory | rename($from, $to) | true / false |
| Copy a file | copy($from, $to) | true / false |
| Delete an empty directory | rmdir($path) | true / false |
All of them emit a warning and return false on failure.
The examples use a working folder:
<?php
$dir = __DIR__ . '/demo';
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents("$dir/report.txt", "Quarterly report\n");
Copying Files: copy()
<?php
$dir = __DIR__ . '/demo';
$copied = copy("$dir/report.txt", "$dir/report-backup.txt");
var_dump($copied);
echo file_get_contents("$dir/report-backup.txt");
Output
bool(true)
Quarterly report
Key Behavior
copy()creates the destination file, or overwrites it without warning if it already exists.- The destination directory must exist.
- The source file is unchanged.
To avoid overwriting an existing backup, check first or include a timestamp in the name:
$backup = sprintf('%s/report-%s.txt', $dir, date('Ymd-His'));
copy("$dir/report.txt", $backup);
Renaming and Moving Files: rename()
rename() changes a file's name, its directory, or both. "Moving" a file is simply renaming it to a path in another directory.
<?php
$dir = __DIR__ . '/demo';
if (!is_dir("$dir/archive")) {
mkdir("$dir/archive");
}
rename("$dir/report-backup.txt", "$dir/report-old.txt"); // rename
rename("$dir/report-old.txt", "$dir/archive/report-2026-q3.txt"); // move
var_dump(file_exists("$dir/report-old.txt"));
var_dump(file_exists("$dir/archive/report-2026-q3.txt"));
Output
bool(false)
bool(true)
Key Behavior
- If the destination file exists, it is overwritten.
- Within the same filesystem,
rename()is very fast: it only changes directory entries, regardless of file size. It is also atomic, which is why it is used to swap in safely written files (see PHP File Create/Write). - Across different filesystems or disks, PHP copies the data and deletes the original, which takes longer.
rename()also works for directories.
For uploaded files, use move_uploaded_file() instead, which additionally verifies that the file really came from an HTTP upload (see $_FILES).
Deleting Files: unlink()
<?php
$dir = __DIR__ . '/demo';
var_dump(unlink("$dir/archive/report-2026-q3.txt"));
var_dump(unlink("$dir/archive/report-2026-q3.txt"));
Output
bool(true)
PHP Warning: unlink(/path/demo/archive/report-2026-q3.txt): No such file or directory in /path/script.php on line 5
bool(false)
The first call deletes the file. The second fails because the file no longer exists. Deleting is permanent — there is no recycle bin.
Deleting Only If the File Exists
<?php
function deleteIfExists(string $path): bool
{
if (!is_file($path)) {
return false;
}
return unlink($path);
}
Another process might delete the file between the is_file() check and unlink(). If that matters (for example, in cleanup jobs running in parallel), treat a failed unlink() on a missing file as success rather than an error.
Deleting Directories
rmdir() deletes only empty directories:
<?php
$dir = __DIR__ . '/demo';
var_dump(rmdir("$dir/archive")); // now empty
Output
bool(true)
To delete a directory with its contents, remove the contents first, from the deepest level upwards:
<?php
function deleteDirectory(string $path): void
{
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
rmdir($path);
}
RecursiveDirectoryIteratorwalks through every file and subfolder;SKIP_DOTSignores the.and..entries.CHILD_FIRSTvisits the contents of a folder before the folder itself, so each folder is empty by the timermdir()runs.
Recursive deletion is powerful and dangerous: make sure the path is exactly the folder you intend to remove (see the next section). Directories are covered in PHP Directories.
Safety: Restrict Operations to a Known Folder
Never pass user input directly to unlink(), rename(), or copy(). A request such as ?file=../../config/database.php could delete or copy files anywhere the PHP process can reach (path traversal). Resolve the real path and check that it lies inside the allowed folder:
<?php
function deleteInside(string $baseDir, string $relativePath): bool
{
$base = realpath($baseDir);
$target = realpath($baseDir . '/' . $relativePath);
if ($base === false || $target === false) {
return false;
}
if (!str_starts_with($target, $base . DIRECTORY_SEPARATOR) || !is_file($target)) {
return false;
}
return unlink($target);
}
$dir = __DIR__ . '/demo';
file_put_contents("$dir/old.txt", 'temporary');
var_dump(deleteInside($dir, 'old.txt'));
var_dump(deleteInside($dir, '../important.php'));
var_dump(deleteInside($dir, 'missing.txt'));
Output
bool(true)
bool(false)
bool(false)
How It Works
realpath()resolves..,., and symbolic links into a canonical absolute path, and returnsfalseif the path does not exist.- The resolved target must start with the base folder's path plus a directory separator. Without the separator, a base of
/app/demowould also accept/app/demo-secrets/.... - Only regular files are deleted, never directories.
In real applications, files are usually identified by a database ID, and the stored file name is generated by the server — so user input never needs to contain a path at all.
Practical Example: Rotating Log Files
Log files grow continuously. Log rotation renames the current log to a dated file and deletes the oldest dated files, keeping only a fixed number:
<?php
function rotateLog(string $logFile, string $date, int $keep): array
{
$dir = dirname($logFile);
$name = pathinfo($logFile, PATHINFO_FILENAME);
if (is_file($logFile) && filesize($logFile) > 0) {
rename($logFile, "$dir/$name-$date.log");
}
$rotated = glob("$dir/$name-*.log") ?: [];
rsort($rotated); // newest first (dates sort correctly as text)
foreach (array_slice($rotated, $keep) as $old) {
unlink($old);
}
return array_map('basename', array_slice($rotated, 0, $keep));
}
$dir = __DIR__ . '/demo/logs';
@mkdir($dir, 0755, true);
foreach (['2026-10-11', '2026-10-12', '2026-10-13', '2026-10-14'] as $oldDate) {
file_put_contents("$dir/app-$oldDate.log", "entries from $oldDate\n");
}
file_put_contents("$dir/app.log", "entries from 2026-10-15\n");
$kept = rotateLog("$dir/app.log", '2026-10-15', 3);
echo "Kept: " . implode(', ', $kept) . "\n";
echo "Remaining files: " . implode(', ', array_map('basename', glob("$dir/*.log"))) . "\n";
Output
Kept: app-2026-10-15.log, app-2026-10-14.log, app-2026-10-13.log
Remaining files: app-2026-10-13.log, app-2026-10-14.log, app-2026-10-15.log
Step-by-Step Explanation
- The current
app.logis renamed toapp-2026-10-15.log(only if it contains data). The next log write creates a freshapp.log. glob()finds all rotated logs. Because the dates are inYYYY-MM-DDformat, sorting the names in reverse order puts the newest first.array_slice($rotated, $keep)selects everything after the newest three; those files are deleted.- The two oldest logs (11 and 12 October) are removed, and three days of history remain.
In production, the operating system's logrotate tool or the rotation features of logging libraries (such as Monolog's RotatingFileHandler) usually handle this; the example shows the underlying file operations.
Common Mistakes
Assuming copy() or rename() Will Not Overwrite
Effect: Existing files are silently replaced.
Fix: Check for existing destinations or use unique names.
Not Checking Return Values
Effect: Failed deletions or moves (permissions, missing folders) go unnoticed.
Fix: Check for false and log or report the problem.
Using rmdir() on Non-Empty Directories
Effect: "Directory not empty" warning.
Fix: Delete the contents first, from the deepest level up.
Using User Input in Paths
Effect: Deletion or copying of arbitrary server files.
Fix: Resolve with realpath() and confirm the target is inside an allowed folder — or avoid user-supplied paths entirely.
Using rename() for Uploads
Effect: No check that the file came from an upload.
Fix: Use move_uploaded_file().
Best Practices
- Check the return value of every file operation.
- Remember that
copy()andrename()overwrite existing destinations. - Use
rename()for fast, atomic moves within one filesystem. - Restrict file operations to known directories and never trust user-supplied paths.
- Delete recursively only after verifying the exact target folder.
Official reference: https://www.php.net/manual/en/function.unlink.php