File Delete / Rename / Copy

Su Subhalakshmi Chandran Updated 05 Nov 2026
6 min read ·Lesson 246 of 248

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.

TaskFunctionReturns
Delete a fileunlink($path)true / false
Rename or move a file or directoryrename($from, $to)true / false
Copy a filecopy($from, $to)true / false
Delete an empty directoryrmdir($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).


<?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);
}
  • RecursiveDirectoryIterator walks through every file and subfolder; SKIP_DOTS ignores the . and .. entries.
  • CHILD_FIRST visits the contents of a folder before the folder itself, so each folder is empty by the time rmdir() 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

  1. realpath() resolves .., ., and symbolic links into a canonical absolute path, and returns false if the path does not exist.
  2. The resolved target must start with the base folder's path plus a directory separator. Without the separator, a base of /app/demo would also accept /app/demo-secrets/....
  3. 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

  1. The current app.log is renamed to app-2026-10-15.log (only if it contains data). The next log write creates a fresh app.log.
  2. glob() finds all rotated logs. Because the dates are in YYYY-MM-DD format, sorting the names in reverse order puts the newest first.
  3. array_slice($rotated, $keep) selects everything after the newest three; those files are deleted.
  4. 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() and rename() 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

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about File Delete / Rename / Copy and get answers drawn from it.

Signed-in readers only.